Compare commits
22 Commits
build_wind
...
nolock.2
Author | SHA1 | Date | |
---|---|---|---|
c74b716e11 | |||
a14b798185 | |||
fd9498e07c | |||
09e8c20168 | |||
da484b7ab9 | |||
063a8339ed | |||
b5c8de08e0 | |||
45fe6b602b | |||
cca2c9ffb7 | |||
22d78e8a82 | |||
bcb8503b26 | |||
9aa6cc71be | |||
54e08b4eae | |||
bd8b945bb3 | |||
a73bf932d6 | |||
7011dd1ef0 | |||
ee6620d123 | |||
aa34d63171 | |||
bf625790fa | |||
db55edb196 | |||
1c3801a0b1 | |||
be3d357a6d |
@ -5,9 +5,9 @@ cd "$DEPS_DIR"
|
||||
. common
|
||||
|
||||
VERSION=1.0.27
|
||||
FILENAME=libusb-$VERSION.tar.bz2
|
||||
FILENAME=libusb-$VERSION.tar.gz
|
||||
PROJECT_DIR=libusb-$VERSION
|
||||
SHA256SUM=ffaa41d741a8a3bee244ac8e54a72ea05bf2879663c098c82fc5757853441575
|
||||
SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de
|
||||
|
||||
cd "$SOURCES_DIR"
|
||||
|
||||
@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
|
||||
then
|
||||
echo "$PWD/$PROJECT_DIR" found
|
||||
else
|
||||
get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/libusb-$VERSION.tar.bz2" "$FILENAME" "$SHA256SUM"
|
||||
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
||||
fi
|
||||
|
||||
@ -33,6 +33,7 @@ else
|
||||
mkdir "$HOST"
|
||||
cd "$HOST"
|
||||
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh
|
||||
"$SOURCES_DIR/$PROJECT_DIR"/configure \
|
||||
--prefix="$INSTALL_DIR/$HOST" \
|
||||
--host="$HOST_TRIPLET" \
|
||||
|
12
app/scrcpy.1
12
app/scrcpy.1
@ -316,6 +316,10 @@ Disable video forwarding.
|
||||
.B \-\-no\-video\-playback
|
||||
Disable video playback on the computer.
|
||||
|
||||
.TP
|
||||
.B \-\-no\-window
|
||||
Disable scrcpy window. Implies --no-video-playback and --no-control.
|
||||
|
||||
.TP
|
||||
.BI "\-\-orientation " value
|
||||
Same as --display-orientation=value --record-orientation=value.
|
||||
@ -577,6 +581,14 @@ Flip display horizontally
|
||||
.B MOD+Shift+Up, MOD+Shift+Down
|
||||
Flip display vertically
|
||||
|
||||
.TP
|
||||
.B MOD+z
|
||||
Pause or re-pause display
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+z
|
||||
Unpause display
|
||||
|
||||
.TP
|
||||
.B MOD+g
|
||||
Resize window to 1:1 (pixel\-perfect)
|
||||
|
@ -66,8 +66,6 @@ static void SDLCALL
|
||||
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||
struct sc_audio_player *ap = userdata;
|
||||
|
||||
// This callback is called with the lock used by SDL_LockAudioDevice()
|
||||
|
||||
assert(len_int > 0);
|
||||
size_t len = len_int;
|
||||
uint32_t count = TO_SAMPLES(len);
|
||||
@ -181,29 +179,19 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
if (written < samples) {
|
||||
uint32_t remaining = samples - written;
|
||||
|
||||
// All samples that could be written without locking have been written,
|
||||
// now we need to lock to drop/consume old samples
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
assert(remaining <= cap);
|
||||
skipped_samples = sc_audiobuf_truncate(&ap->buf, cap - remaining);
|
||||
|
||||
// Retry with the lock
|
||||
written += sc_audiobuf_write(&ap->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
if (written < samples) {
|
||||
remaining = samples - written;
|
||||
// Still insufficient, drop old samples to make space
|
||||
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
|
||||
assert(skipped_samples == remaining);
|
||||
LOGW("Audio buffer full, %" PRIu32 " samples dropped", skipped_samples);
|
||||
|
||||
// Now there is enough space
|
||||
uint32_t w = sc_audiobuf_write(&ap->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
assert(w == remaining);
|
||||
(void) w;
|
||||
}
|
||||
// Now there is enough space
|
||||
uint32_t w = sc_audiobuf_write(&ap->buf,
|
||||
swr_buf + TO_BYTES(written),
|
||||
remaining);
|
||||
assert(w == remaining);
|
||||
(void) w;
|
||||
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
written = samples;
|
||||
}
|
||||
|
||||
uint32_t underflow = 0;
|
||||
@ -225,30 +213,22 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
|
||||
uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
uint32_t skip_samples = 0;
|
||||
uint32_t skipped = sc_audiobuf_truncate(&ap->buf, max_buffered_samples);
|
||||
assert(skipped);
|
||||
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
can_read = sc_audiobuf_can_read(&ap->buf);
|
||||
if (can_read > max_buffered_samples) {
|
||||
skip_samples = can_read - max_buffered_samples;
|
||||
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
|
||||
assert(r == skip_samples);
|
||||
(void) r;
|
||||
skipped_samples += skip_samples;
|
||||
}
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
|
||||
if (skip_samples) {
|
||||
if (played) {
|
||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
if (played) {
|
||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||
" samples", skipped);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
} else {
|
||||
LOGD("[Audio] Playback not started, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
} else {
|
||||
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
|
||||
skipped);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
skipped_samples += skipped;
|
||||
|
||||
can_read = sc_audiobuf_can_read(&ap->buf);
|
||||
}
|
||||
|
||||
atomic_store_explicit(&ap->received, true, memory_order_relaxed);
|
||||
@ -408,7 +388,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
||||
// producer and the consumer. It's too big on purpose, to guarantee that
|
||||
// the producer and the consumer will be able to access it in parallel
|
||||
// without locking.
|
||||
// without dropping samples.
|
||||
uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
|
||||
|
||||
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
|
||||
|
@ -97,6 +97,7 @@ enum {
|
||||
OPT_MOUSE,
|
||||
OPT_HID_KEYBOARD_DEPRECATED,
|
||||
OPT_HID_MOUSE_DEPRECATED,
|
||||
OPT_NO_WINDOW,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -566,6 +567,12 @@ static const struct sc_option options[] = {
|
||||
.longopt = "no-video-playback",
|
||||
.text = "Disable video playback on the computer.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_WINDOW,
|
||||
.longopt = "no-window",
|
||||
.text = "Disable scrcpy window. Implies --no-video-playback and "
|
||||
"--no-control.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ORIENTATION,
|
||||
.longopt = "orientation",
|
||||
@ -900,6 +907,14 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
.shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" },
|
||||
.text = "Flip display vertically",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+z" },
|
||||
.text = "Pause or re-pause display",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+z" },
|
||||
.text = "Unpause display",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+g" },
|
||||
.text = "Resize window to 1:1 (pixel-perfect)",
|
||||
@ -2478,6 +2493,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_CAMERA_HIGH_SPEED:
|
||||
opts->camera_high_speed = true;
|
||||
break;
|
||||
case OPT_NO_WINDOW:
|
||||
opts->window = false;
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -2515,6 +2533,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
v4l2 = !!opts->v4l2_device;
|
||||
#endif
|
||||
|
||||
if (!opts->window) {
|
||||
// Without window, there cannot be any video playback or control
|
||||
opts->video_playback = false;
|
||||
opts->control = false;
|
||||
}
|
||||
|
||||
if (!opts->video) {
|
||||
opts->video_playback = false;
|
||||
// Do not power on the device on start if video capture is disabled
|
||||
@ -2536,8 +2560,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->audio = false;
|
||||
}
|
||||
|
||||
if (!opts->video && !opts->audio && !otg) {
|
||||
LOGE("No video, no audio, no OTG: nothing to do");
|
||||
if (!opts->video && !opts->audio && !opts->control && !otg) {
|
||||
LOGE("No video, no audio, no control, no OTG: nothing to do");
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -2548,9 +2572,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
|
||||
if (opts->audio_playback && opts->audio_buffer == -1) {
|
||||
if (opts->audio_codec == SC_CODEC_FLAC) {
|
||||
// Use 50 ms audio buffer by default, but use a higher value for FLAC,
|
||||
// which is not low latency (the default encoder produces blocks of
|
||||
// 4096 samples, which represent ~85.333ms).
|
||||
// Use 50 ms audio buffer by default, but use a higher value for
|
||||
// FLAC, which is not low latency (the default encoder produces
|
||||
// blocks of 4096 samples, which represent ~85.333ms).
|
||||
LOGI("FLAC audio: audio buffer increased to 120 ms (use "
|
||||
"--audio-buffer to set a custom value)");
|
||||
opts->audio_buffer = SC_TICK_FROM_MS(120);
|
||||
@ -2561,6 +2585,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
if (v4l2) {
|
||||
if (!opts->video) {
|
||||
LOGE("V4L2 sink requires video capture, but --no-video was set.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts->lock_video_orientation ==
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||
LOGI("Video orientation is locked for v4l2 sink. "
|
||||
@ -2580,16 +2609,33 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
#endif
|
||||
|
||||
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
|
||||
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
|
||||
: SC_KEYBOARD_INPUT_MODE_SDK;
|
||||
}
|
||||
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
|
||||
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
|
||||
: SC_MOUSE_INPUT_MODE_SDK;
|
||||
if (opts->control) {
|
||||
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
|
||||
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
|
||||
: SC_KEYBOARD_INPUT_MODE_SDK;
|
||||
}
|
||||
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
|
||||
if (otg) {
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
|
||||
} else if (!opts->video_playback) {
|
||||
LOGI("No video mirroring, mouse mode switched to UHID");
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
|
||||
} else {
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
|
||||
}
|
||||
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK
|
||||
&& !opts->video_playback) {
|
||||
LOGE("SDK mouse mode requires video playback. Try --mouse=uhid.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (otg) {
|
||||
if (!opts->control) {
|
||||
LOGE("--no-control is not allowed in OTG mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode;
|
||||
if (kmode != SC_KEYBOARD_INPUT_MODE_AOA
|
||||
&& kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) {
|
||||
@ -2692,6 +2738,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->record_filename) {
|
||||
if (!opts->video && !opts->audio) {
|
||||
LOGE("Video and audio disabled, nothing to record");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opts->record_format) {
|
||||
opts->record_format = guess_record_format(opts->record_filename);
|
||||
if (!opts->record_format) {
|
||||
@ -2798,6 +2849,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
# endif
|
||||
|
||||
if (opts->start_fps_counter && !opts->video_playback) {
|
||||
LOGW("--print-fps has no effect without video playback");
|
||||
opts->start_fps_counter = false;
|
||||
}
|
||||
|
||||
if (otg) {
|
||||
// OTG mode is compatible with only very few options.
|
||||
// Only report obvious errors.
|
||||
|
@ -6,8 +6,19 @@
|
||||
|
||||
#define SC_CONTROL_MSG_QUEUE_MAX 64
|
||||
|
||||
static void
|
||||
sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) {
|
||||
(void) receiver;
|
||||
|
||||
struct sc_controller *controller = userdata;
|
||||
// Forward the event to the controller listener
|
||||
controller->cbs->on_error(controller, controller->cbs_userdata);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
|
||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||
const struct sc_controller_callbacks *cbs,
|
||||
void *cbs_userdata) {
|
||||
sc_vecdeque_init(&controller->queue);
|
||||
|
||||
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
|
||||
@ -15,7 +26,12 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_receiver_init(&controller->receiver, control_socket);
|
||||
static const struct sc_receiver_callbacks receiver_cbs = {
|
||||
.on_error = sc_controller_receiver_on_error,
|
||||
};
|
||||
|
||||
ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs,
|
||||
controller);
|
||||
if (!ok) {
|
||||
sc_vecdeque_destroy(&controller->queue);
|
||||
return false;
|
||||
@ -39,6 +55,10 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
|
||||
controller->control_socket = control_socket;
|
||||
controller->stopped = false;
|
||||
|
||||
assert(cbs && cbs->on_error);
|
||||
controller->cbs = cbs;
|
||||
controller->cbs_userdata = cbs_userdata;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -125,10 +145,16 @@ run_controller(void *data) {
|
||||
sc_control_msg_destroy(&msg);
|
||||
if (!ok) {
|
||||
LOGD("Could not write msg to socket");
|
||||
break;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
controller->cbs->on_error(controller, controller->cbs_userdata);
|
||||
|
||||
return 1; // ignored
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -22,10 +22,19 @@ struct sc_controller {
|
||||
bool stopped;
|
||||
struct sc_control_msg_queue queue;
|
||||
struct sc_receiver receiver;
|
||||
|
||||
const struct sc_controller_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_controller_callbacks {
|
||||
void (*on_error)(struct sc_controller *controller, void *userdata);
|
||||
};
|
||||
|
||||
bool
|
||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket);
|
||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||
const struct sc_controller_callbacks *cbs,
|
||||
void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_controller_configure(struct sc_controller *controller,
|
||||
|
@ -1,11 +1,34 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
static bool
|
||||
sc_display_init_novideo_icon(struct sc_display *display,
|
||||
SDL_Surface *icon_novideo) {
|
||||
assert(icon_novideo);
|
||||
|
||||
if (SDL_RenderSetLogicalSize(display->renderer,
|
||||
icon_novideo->w, icon_novideo->h)) {
|
||||
LOGW("Could not set renderer logical size: %s", SDL_GetError());
|
||||
// don't fail
|
||||
}
|
||||
|
||||
display->texture = SDL_CreateTextureFromSurface(display->renderer,
|
||||
icon_novideo);
|
||||
if (!display->texture) {
|
||||
LOGE("Could not create texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
||||
SDL_Surface *icon_novideo, bool mipmaps) {
|
||||
display->renderer =
|
||||
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (!display->renderer) {
|
||||
@ -65,6 +88,19 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
|
||||
display->texture = NULL;
|
||||
display->pending.flags = 0;
|
||||
display->pending.frame = NULL;
|
||||
display->has_frame = false;
|
||||
|
||||
if (icon_novideo) {
|
||||
// Without video, set a static scrcpy icon as window content
|
||||
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
|
||||
if (!ok) {
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GL_DeleteContext(display->gl_context);
|
||||
#endif
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -196,9 +232,25 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
|
||||
return SC_DISPLAY_RESULT_OK;
|
||||
}
|
||||
|
||||
static SDL_YUV_CONVERSION_MODE
|
||||
sc_display_to_sdl_color_range(enum AVColorRange color_range) {
|
||||
return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG
|
||||
: SDL_YUV_CONVERSION_AUTOMATIC;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_display_update_texture_internal(struct sc_display *display,
|
||||
const AVFrame *frame) {
|
||||
if (!display->has_frame) {
|
||||
// First frame
|
||||
display->has_frame = true;
|
||||
|
||||
// Configure YUV color range conversion
|
||||
SDL_YUV_CONVERSION_MODE sdl_color_range =
|
||||
sc_display_to_sdl_color_range(frame->color_range);
|
||||
SDL_SetYUVConversionMode(sdl_color_range);
|
||||
}
|
||||
|
||||
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
|
||||
frame->data[0], frame->linesize[0],
|
||||
frame->data[1], frame->linesize[1],
|
||||
|
@ -33,6 +33,8 @@ struct sc_display {
|
||||
struct sc_size size;
|
||||
AVFrame *frame;
|
||||
} pending;
|
||||
|
||||
bool has_frame;
|
||||
};
|
||||
|
||||
enum sc_display_result {
|
||||
@ -42,7 +44,8 @@ enum sc_display_result {
|
||||
};
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
||||
SDL_Surface *icon_novideo, bool mipmaps);
|
||||
|
||||
void
|
||||
sc_display_destroy(struct sc_display *display);
|
||||
|
@ -7,3 +7,4 @@
|
||||
#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)
|
||||
#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9)
|
||||
|
@ -402,6 +402,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
const SDL_KeyboardEvent *event) {
|
||||
// controller is NULL if --no-control is requested
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
bool video = im->screen->video;
|
||||
|
||||
SDL_Keycode keycode = event->keysym.sym;
|
||||
uint16_t mod = event->keysym.mod;
|
||||
@ -427,63 +429,68 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
|
||||
switch (keycode) {
|
||||
case SDLK_h:
|
||||
if (im->kp && !shift && !repeat) {
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_b: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (im->kp && !shift && !repeat) {
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_back(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_s:
|
||||
if (im->kp && !shift && !repeat) {
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_app_switch(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_m:
|
||||
if (im->kp && !shift && !repeat) {
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_menu(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_p:
|
||||
if (im->kp && !shift && !repeat) {
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_power(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_o:
|
||||
if (control && !repeat && down) {
|
||||
if (control && !repeat && down && !paused) {
|
||||
enum sc_screen_power_mode mode = shift
|
||||
? SC_SCREEN_POWER_MODE_NORMAL
|
||||
: SC_SCREEN_POWER_MODE_OFF;
|
||||
set_screen_power_mode(im, mode);
|
||||
}
|
||||
return;
|
||||
case SDLK_z:
|
||||
if (video && down && !repeat) {
|
||||
sc_screen_set_paused(im->screen, !shift);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (shift) {
|
||||
if (!repeat & down) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp) {
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_down(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (shift) {
|
||||
if (!repeat & down) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp) {
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_up(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_LEFT:
|
||||
if (!repeat && down) {
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
@ -494,7 +501,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_RIGHT:
|
||||
if (!repeat && down) {
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
@ -505,17 +512,17 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_c:
|
||||
if (im->kp && !shift && !repeat && down) {
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_x:
|
||||
if (im->kp && !shift && !repeat && down) {
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_v:
|
||||
if (im->kp && !repeat && down) {
|
||||
if (im->kp && !repeat && down && !paused) {
|
||||
if (shift || im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
clipboard_paste(im);
|
||||
@ -527,27 +534,27 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_f:
|
||||
if (!shift && !repeat && down) {
|
||||
if (video && !shift && !repeat && down) {
|
||||
sc_screen_switch_fullscreen(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_w:
|
||||
if (!shift && !repeat && down) {
|
||||
if (video && !shift && !repeat && down) {
|
||||
sc_screen_resize_to_fit(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_g:
|
||||
if (!shift && !repeat && down) {
|
||||
if (video && !shift && !repeat && down) {
|
||||
sc_screen_resize_to_pixel_perfect(im->screen);
|
||||
}
|
||||
return;
|
||||
case SDLK_i:
|
||||
if (!shift && !repeat && down) {
|
||||
if (video && !shift && !repeat && down) {
|
||||
switch_fps_counter_state(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_n:
|
||||
if (control && !repeat && down) {
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
collapse_panels(im);
|
||||
} else if (im->key_repeat == 0) {
|
||||
@ -558,12 +565,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
}
|
||||
return;
|
||||
case SDLK_r:
|
||||
if (control && !shift && !repeat && down) {
|
||||
if (control && !shift && !repeat && down && !paused) {
|
||||
rotate_device(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_k:
|
||||
if (control && !shift && !repeat && down
|
||||
if (control && !shift && !repeat && down && !paused
|
||||
&& im->kp && im->kp->hid) {
|
||||
// Only if the current keyboard is hid
|
||||
open_hard_keyboard_settings(im);
|
||||
@ -574,7 +581,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->kp) {
|
||||
if (!im->kp || paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -619,22 +626,33 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
im->kp->ops->process_key(im->kp, &evt, ack_to_wait);
|
||||
}
|
||||
|
||||
static struct sc_position
|
||||
sc_input_manager_get_position(struct sc_input_manager *im, int32_t x,
|
||||
int32_t y) {
|
||||
if (im->mp->relative_mode) {
|
||||
// No absolute position
|
||||
return (struct sc_position) {
|
||||
.screen_size = {0, 0},
|
||||
.point = {0, 0},
|
||||
};
|
||||
}
|
||||
|
||||
return (struct sc_position) {
|
||||
.screen_size = im->screen->frame_size,
|
||||
.point = sc_screen_convert_window_to_frame_coords(im->screen, x, y),
|
||||
};
|
||||
}
|
||||
|
||||
static void
|
||||
sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
const SDL_MouseMotionEvent *event) {
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_mouse_motion_event evt = {
|
||||
.position = {
|
||||
.screen_size = im->screen->frame_size,
|
||||
.point = sc_screen_convert_window_to_frame_coords(im->screen,
|
||||
event->x,
|
||||
event->y),
|
||||
},
|
||||
.position = sc_input_manager_get_position(im, event->x, event->y),
|
||||
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
|
||||
: POINTER_ID_GENERIC_FINGER,
|
||||
.xrel = event->xrel,
|
||||
@ -695,16 +713,16 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
const SDL_MouseButtonEvent *event) {
|
||||
bool control = im->controller;
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
}
|
||||
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
bool down = event->type == SDL_MOUSEBUTTONDOWN;
|
||||
if (!im->forward_all_clicks) {
|
||||
if (control) {
|
||||
if (control && !paused) {
|
||||
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
|
||||
|
||||
if (im->kp && event->button == SDL_BUTTON_X1) {
|
||||
@ -730,7 +748,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
// double-click on black borders resize to fit the device screen
|
||||
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||
bool video = im->screen->video;
|
||||
if (video && event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
|
||||
int32_t x = event->x;
|
||||
int32_t y = event->y;
|
||||
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
|
||||
@ -747,19 +766,14 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
// otherwise, send the click event to the device
|
||||
}
|
||||
|
||||
if (!im->mp) {
|
||||
if (!im->mp || paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
|
||||
|
||||
struct sc_mouse_click_event evt = {
|
||||
.position = {
|
||||
.screen_size = im->screen->frame_size,
|
||||
.point = sc_screen_convert_window_to_frame_coords(im->screen,
|
||||
event->x,
|
||||
event->y),
|
||||
},
|
||||
.position = sc_input_manager_get_position(im, event->x, event->y),
|
||||
.action = sc_action_from_sdl_mousebutton_type(event->type),
|
||||
.button = sc_mouse_button_from_sdl(event->button),
|
||||
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
|
||||
@ -834,11 +848,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
|
||||
|
||||
struct sc_mouse_scroll_event evt = {
|
||||
.position = {
|
||||
.screen_size = im->screen->frame_size,
|
||||
.point = sc_screen_convert_window_to_frame_coords(im->screen,
|
||||
mouse_x, mouse_y),
|
||||
},
|
||||
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 18)
|
||||
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
|
||||
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
|
||||
@ -885,9 +895,10 @@ void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event) {
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
switch (event->type) {
|
||||
case SDL_TEXTINPUT:
|
||||
if (!im->kp) {
|
||||
if (!im->kp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_text_input(im, &event->text);
|
||||
@ -899,13 +910,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
sc_input_manager_process_key(im, &event->key);
|
||||
break;
|
||||
case SDL_MOUSEMOTION:
|
||||
if (!im->mp) {
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_motion(im, &event->motion);
|
||||
break;
|
||||
case SDL_MOUSEWHEEL:
|
||||
if (!im->mp) {
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_wheel(im, &event->wheel);
|
||||
@ -919,7 +930,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
case SDL_FINGERMOTION:
|
||||
case SDL_FINGERDOWN:
|
||||
case SDL_FINGERUP:
|
||||
if (!im->mp) {
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_touch(im, &event->tfinger);
|
||||
|
@ -89,6 +89,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.kill_adb_on_close = false,
|
||||
.camera_high_speed = false,
|
||||
.list = 0,
|
||||
.window = true,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
@ -279,6 +279,7 @@ struct scrcpy_options {
|
||||
#define SC_OPTION_LIST_CAMERAS 0x4
|
||||
#define SC_OPTION_LIST_CAMERA_SIZES 0x8
|
||||
uint8_t list;
|
||||
bool window;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
@ -10,7 +10,8 @@
|
||||
#include "util/str.h"
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) {
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
const struct sc_receiver_callbacks *cbs, void *cbs_userdata) {
|
||||
bool ok = sc_mutex_init(&receiver->mutex);
|
||||
if (!ok) {
|
||||
return false;
|
||||
@ -20,6 +21,10 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) {
|
||||
receiver->acksync = NULL;
|
||||
receiver->uhid_devices = NULL;
|
||||
|
||||
assert(cbs && cbs->on_error);
|
||||
receiver->cbs = cbs;
|
||||
receiver->cbs_userdata = cbs_userdata;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -152,6 +157,8 @@ run_receiver(void *data) {
|
||||
}
|
||||
}
|
||||
|
||||
receiver->cbs->on_error(receiver, receiver->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -19,10 +19,18 @@ struct sc_receiver {
|
||||
|
||||
struct sc_acksync *acksync;
|
||||
struct sc_uhid_devices *uhid_devices;
|
||||
|
||||
const struct sc_receiver_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_receiver_callbacks {
|
||||
void (*on_error)(struct sc_receiver *receiver, void *userdata);
|
||||
};
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket);
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
const struct sc_receiver_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_receiver_destroy(struct sc_receiver *receiver);
|
||||
|
@ -174,6 +174,9 @@ event_loop(struct scrcpy *s) {
|
||||
case SC_EVENT_DEMUXER_ERROR:
|
||||
LOGE("Demuxer error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SC_EVENT_CONTROLLER_ERROR:
|
||||
LOGE("Controller error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SC_EVENT_RECORDER_ERROR:
|
||||
LOGE("Recorder error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
@ -265,6 +268,16 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_controller_on_error(struct sc_controller *controller, void *userdata) {
|
||||
// Note: this function may be called twice, once from the controller thread
|
||||
// and once from the receiver thread
|
||||
(void) controller;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
@ -408,7 +421,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (options->video_playback) {
|
||||
if (options->window) {
|
||||
// Set hints before starting the server thread to avoid race conditions
|
||||
// in SDL
|
||||
sdl_set_hints(options->render_driver);
|
||||
@ -430,7 +443,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
assert(!options->video_playback || options->video);
|
||||
assert(!options->audio_playback || options->audio);
|
||||
|
||||
if (options->video_playback ||
|
||||
if (options->window ||
|
||||
(options->control && options->clipboard_autosync)) {
|
||||
// Initialize the video subsystem even if --no-video or
|
||||
// --no-video-playback is passed so that clipboard synchronization
|
||||
@ -553,7 +566,12 @@ scrcpy(struct scrcpy_options *options) {
|
||||
struct sc_mouse_processor *mp = NULL;
|
||||
|
||||
if (options->control) {
|
||||
if (!sc_controller_init(&s->controller, s->server.control_socket)) {
|
||||
static const struct sc_controller_callbacks controller_cbs = {
|
||||
.on_error = sc_controller_on_error,
|
||||
};
|
||||
|
||||
if (!sc_controller_init(&s->controller, s->server.control_socket,
|
||||
&controller_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
controller_initialized = true;
|
||||
@ -684,11 +702,12 @@ scrcpy(struct scrcpy_options *options) {
|
||||
// There is a controller if and only if control is enabled
|
||||
assert(options->control == !!controller);
|
||||
|
||||
if (options->video_playback) {
|
||||
if (options->window) {
|
||||
const char *window_title =
|
||||
options->window_title ? options->window_title : info->device_name;
|
||||
|
||||
struct sc_screen_params screen_params = {
|
||||
.video = options->video_playback,
|
||||
.controller = controller,
|
||||
.fp = fp,
|
||||
.kp = kp,
|
||||
@ -710,12 +729,15 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.start_fps_counter = options->start_fps_counter,
|
||||
};
|
||||
|
||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||
if (options->display_buffer) {
|
||||
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
|
||||
true);
|
||||
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
||||
src = &s->display_buffer.frame_source;
|
||||
struct sc_frame_source *src;
|
||||
if (options->video_playback) {
|
||||
src = &s->video_decoder.frame_source;
|
||||
if (options->display_buffer) {
|
||||
sc_delay_buffer_init(&s->display_buffer,
|
||||
options->display_buffer, true);
|
||||
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
||||
src = &s->display_buffer.frame_source;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sc_screen_init(&s->screen, &screen_params)) {
|
||||
@ -723,7 +745,9 @@ scrcpy(struct scrcpy_options *options) {
|
||||
}
|
||||
screen_initialized = true;
|
||||
|
||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||
if (options->video_playback) {
|
||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||
}
|
||||
}
|
||||
|
||||
if (options->audio_playback) {
|
||||
@ -805,9 +829,12 @@ scrcpy(struct scrcpy_options *options) {
|
||||
ret = event_loop(s);
|
||||
LOGD("quit...");
|
||||
|
||||
// Close the window immediately on closing, because screen_destroy() may
|
||||
// only be called once the video demuxer thread is joined (it may take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
if (options->video_playback) {
|
||||
// Close the window immediately on closing, because screen_destroy()
|
||||
// may only be called once the video demuxer thread is joined (it may
|
||||
// take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
}
|
||||
|
||||
end:
|
||||
if (timeout_started) {
|
||||
|
178
app/src/screen.c
178
app/src/screen.c
@ -205,6 +205,8 @@ sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
|
||||
|
||||
static void
|
||||
sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
int dw;
|
||||
int dh;
|
||||
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
|
||||
@ -246,6 +248,8 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
// changed, so that the content rectangle is recomputed
|
||||
static void
|
||||
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
assert(screen->video);
|
||||
|
||||
if (update_content_rect) {
|
||||
sc_screen_update_content_rect(screen);
|
||||
}
|
||||
@ -255,6 +259,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
(void) res; // any error already logged
|
||||
}
|
||||
|
||||
static void
|
||||
sc_screen_render_novideo(struct sc_screen *screen) {
|
||||
enum sc_display_result res =
|
||||
sc_display_render(&screen->display, NULL, SC_ORIENTATION_0);
|
||||
(void) res; // any error already logged
|
||||
}
|
||||
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
@ -268,6 +279,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
static int
|
||||
event_watcher(void *data, SDL_Event *event) {
|
||||
struct sc_screen *screen = data;
|
||||
assert(screen->video);
|
||||
|
||||
if (event->type == SDL_WINDOWEVENT
|
||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
// In practice, it seems to always be called from the same thread in
|
||||
@ -326,6 +339,7 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
static bool
|
||||
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
assert(screen->video);
|
||||
|
||||
bool previous_skipped;
|
||||
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
|
||||
@ -362,6 +376,11 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
screen->paused = false;
|
||||
screen->resume_frame = NULL;
|
||||
screen->orientation = SC_ORIENTATION_0;
|
||||
|
||||
screen->video = params->video;
|
||||
|
||||
screen->req.x = params->window_x;
|
||||
screen->req.y = params->window_y;
|
||||
@ -379,41 +398,75 @@ sc_screen_init(struct sc_screen *screen,
|
||||
goto error_destroy_frame_buffer;
|
||||
}
|
||||
|
||||
screen->orientation = params->orientation;
|
||||
if (screen->orientation != SC_ORIENTATION_0) {
|
||||
LOGI("Initial display orientation set to %s",
|
||||
sc_orientation_get_name(screen->orientation));
|
||||
if (screen->video) {
|
||||
screen->orientation = params->orientation;
|
||||
if (screen->orientation != SC_ORIENTATION_0) {
|
||||
LOGI("Initial display orientation set to %s",
|
||||
sc_orientation_get_name(screen->orientation));
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t window_flags = SDL_WINDOW_HIDDEN
|
||||
| SDL_WINDOW_RESIZABLE
|
||||
| SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
if (params->always_on_top) {
|
||||
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
|
||||
}
|
||||
if (params->window_borderless) {
|
||||
window_flags |= SDL_WINDOW_BORDERLESS;
|
||||
}
|
||||
if (params->video) {
|
||||
// The window will be shown on first frame
|
||||
window_flags |= SDL_WINDOW_HIDDEN
|
||||
| SDL_WINDOW_RESIZABLE;
|
||||
}
|
||||
|
||||
const char *title = params->window_title;
|
||||
assert(title);
|
||||
|
||||
int x = SDL_WINDOWPOS_UNDEFINED;
|
||||
int y = SDL_WINDOWPOS_UNDEFINED;
|
||||
int width = 256;
|
||||
int height = 256;
|
||||
if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) {
|
||||
x = params->window_x;
|
||||
}
|
||||
if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) {
|
||||
y = params->window_y;
|
||||
}
|
||||
if (params->window_width) {
|
||||
width = params->window_width;
|
||||
}
|
||||
if (params->window_height) {
|
||||
height = params->window_height;
|
||||
}
|
||||
|
||||
// The window will be positioned and sized on first video frame
|
||||
screen->window =
|
||||
SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags);
|
||||
screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags);
|
||||
if (!screen->window) {
|
||||
LOGE("Could not create window: %s", SDL_GetError());
|
||||
goto error_destroy_fps_counter;
|
||||
}
|
||||
|
||||
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
|
||||
if (!ok) {
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_Surface *icon = scrcpy_icon_load();
|
||||
if (icon) {
|
||||
SDL_SetWindowIcon(screen->window, icon);
|
||||
scrcpy_icon_destroy(icon);
|
||||
} else {
|
||||
} else if (params->video) {
|
||||
// just a warning
|
||||
LOGW("Could not load icon");
|
||||
} else {
|
||||
// without video, the icon is used as window content, it must be present
|
||||
LOGE("Could not load icon");
|
||||
goto error_destroy_fps_counter;
|
||||
}
|
||||
|
||||
SDL_Surface *icon_novideo = params->video ? NULL : icon;
|
||||
bool mipmaps = params->video && params->mipmaps;
|
||||
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
|
||||
mipmaps);
|
||||
if (icon) {
|
||||
scrcpy_icon_destroy(icon);
|
||||
}
|
||||
if (!ok) {
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
screen->frame = av_frame_alloc();
|
||||
@ -437,7 +490,9 @@ sc_screen_init(struct sc_screen *screen,
|
||||
sc_input_manager_init(&screen->im, &im_params);
|
||||
|
||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||
SDL_AddEventWatch(event_watcher, screen);
|
||||
if (screen->video) {
|
||||
SDL_AddEventWatch(event_watcher, screen);
|
||||
}
|
||||
#endif
|
||||
|
||||
static const struct sc_frame_sink_ops ops = {
|
||||
@ -452,6 +507,11 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->open = false;
|
||||
#endif
|
||||
|
||||
if (!screen->video && sc_screen_is_relative_mode(screen)) {
|
||||
// Capture mouse immediately if video mirroring is disabled
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_display:
|
||||
@ -522,6 +582,8 @@ sc_screen_destroy(struct sc_screen *screen) {
|
||||
static void
|
||||
resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
|
||||
struct sc_size new_content_size) {
|
||||
assert(screen->video);
|
||||
|
||||
struct sc_size window_size = get_window_size(screen);
|
||||
struct sc_size target_size = {
|
||||
.width = (uint32_t) window_size.width * new_content_size.width
|
||||
@ -535,6 +597,8 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
|
||||
|
||||
static void
|
||||
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
|
||||
assert(screen->video);
|
||||
|
||||
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
|
||||
resize_for_content(screen, screen->content_size, new_content_size);
|
||||
} else if (!screen->resize_pending) {
|
||||
@ -549,6 +613,8 @@ set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
|
||||
|
||||
static void
|
||||
apply_pending_resize(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
assert(!screen->fullscreen);
|
||||
assert(!screen->maximized);
|
||||
assert(!screen->minimized);
|
||||
@ -562,6 +628,8 @@ apply_pending_resize(struct sc_screen *screen) {
|
||||
void
|
||||
sc_screen_set_orientation(struct sc_screen *screen,
|
||||
enum sc_orientation orientation) {
|
||||
assert(screen->video);
|
||||
|
||||
if (orientation == screen->orientation) {
|
||||
return;
|
||||
}
|
||||
@ -596,6 +664,8 @@ sc_screen_init_size(struct sc_screen *screen) {
|
||||
// recreate the texture and resize the window if the frame size has changed
|
||||
static enum sc_display_result
|
||||
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->frame_size.width == new_frame_size.width
|
||||
&& screen->frame_size.height == new_frame_size.height) {
|
||||
return SC_DISPLAY_RESULT_OK;
|
||||
@ -614,13 +684,12 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_screen_update_frame(struct sc_screen *screen) {
|
||||
av_frame_unref(screen->frame);
|
||||
sc_frame_buffer_consume(&screen->fb, screen->frame);
|
||||
AVFrame *frame = screen->frame;
|
||||
sc_screen_apply_frame(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
||||
|
||||
AVFrame *frame = screen->frame;
|
||||
struct sc_size new_frame_size = {frame->width, frame->height};
|
||||
enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
|
||||
if (res == SC_DISPLAY_RESULT_ERROR) {
|
||||
@ -655,8 +724,62 @@ sc_screen_update_frame(struct sc_screen *screen) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_screen_update_frame(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->paused) {
|
||||
if (!screen->resume_frame) {
|
||||
screen->resume_frame = av_frame_alloc();
|
||||
if (!screen->resume_frame) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
av_frame_unref(screen->resume_frame);
|
||||
}
|
||||
sc_frame_buffer_consume(&screen->fb, screen->resume_frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
av_frame_unref(screen->frame);
|
||||
sc_frame_buffer_consume(&screen->fb, screen->frame);
|
||||
return sc_screen_apply_frame(screen);
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_set_paused(struct sc_screen *screen, bool paused) {
|
||||
assert(screen->video);
|
||||
|
||||
if (!paused && !screen->paused) {
|
||||
// nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen->paused && screen->resume_frame) {
|
||||
// If display screen was paused, refresh the frame immediately, even if
|
||||
// the new state is also paused.
|
||||
av_frame_free(&screen->frame);
|
||||
screen->frame = screen->resume_frame;
|
||||
screen->resume_frame = NULL;
|
||||
sc_screen_apply_frame(screen);
|
||||
}
|
||||
|
||||
if (!paused) {
|
||||
LOGI("Display screen unpaused");
|
||||
} else if (!screen->paused) {
|
||||
LOGI("Display screen paused");
|
||||
} else {
|
||||
LOGI("Display screen re-paused");
|
||||
}
|
||||
|
||||
screen->paused = paused;
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_switch_fullscreen(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
|
||||
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
||||
@ -674,6 +797,8 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
|
||||
|
||||
void
|
||||
sc_screen_resize_to_fit(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->fullscreen || screen->maximized || screen->minimized) {
|
||||
return;
|
||||
}
|
||||
@ -698,6 +823,8 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
|
||||
|
||||
void
|
||||
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (screen->fullscreen || screen->minimized) {
|
||||
return;
|
||||
}
|
||||
@ -741,6 +868,13 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
return true;
|
||||
}
|
||||
case SDL_WINDOWEVENT:
|
||||
if (!screen->video
|
||||
&& event->window.event == SDL_WINDOWEVENT_EXPOSED) {
|
||||
sc_screen_render_novideo(screen);
|
||||
}
|
||||
|
||||
// !video implies !has_frame
|
||||
assert(screen->video || !screen->has_frame);
|
||||
if (!screen->has_frame) {
|
||||
// Do nothing
|
||||
return true;
|
||||
@ -844,6 +978,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
struct sc_point
|
||||
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
|
||||
int32_t x, int32_t y) {
|
||||
assert(screen->video);
|
||||
|
||||
enum sc_orientation orientation = screen->orientation;
|
||||
|
||||
int32_t w = screen->content_size.width;
|
||||
|
@ -26,6 +26,8 @@ struct sc_screen {
|
||||
bool open; // track the open/close state to assert correct behavior
|
||||
#endif
|
||||
|
||||
bool video;
|
||||
|
||||
struct sc_display display;
|
||||
struct sc_input_manager im;
|
||||
struct sc_frame_buffer fb;
|
||||
@ -64,9 +66,14 @@ struct sc_screen {
|
||||
SDL_Keycode mouse_capture_key_pressed;
|
||||
|
||||
AVFrame *frame;
|
||||
|
||||
bool paused;
|
||||
AVFrame *resume_frame;
|
||||
};
|
||||
|
||||
struct sc_screen_params {
|
||||
bool video;
|
||||
|
||||
struct sc_controller *controller;
|
||||
struct sc_file_pusher *fp;
|
||||
struct sc_key_processor *kp;
|
||||
@ -135,6 +142,10 @@ void
|
||||
sc_screen_set_orientation(struct sc_screen *screen,
|
||||
enum sc_orientation orientation);
|
||||
|
||||
// set the display pause state
|
||||
void
|
||||
sc_screen_set_paused(struct sc_screen *screen, bool paused);
|
||||
|
||||
// react to SDL events
|
||||
// If this function returns false, scrcpy must exit with an error.
|
||||
bool
|
||||
|
@ -37,34 +37,35 @@ sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) {
|
||||
assert(samples_count);
|
||||
|
||||
uint8_t *to = to_;
|
||||
assert(to);
|
||||
|
||||
// Only the reader thread can write tail without synchronization, so
|
||||
// memory_order_relaxed is sufficient
|
||||
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed);
|
||||
// The tail cursor may be updated by the writer thread to drop samples
|
||||
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
|
||||
|
||||
// The head cursor is updated after the data is written to the array
|
||||
uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
|
||||
|
||||
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
|
||||
if (!can_read) {
|
||||
return 0;
|
||||
}
|
||||
if (samples_count > can_read) {
|
||||
samples_count = can_read;
|
||||
}
|
||||
|
||||
if (to) {
|
||||
uint32_t right_count = buf->alloc_size - tail;
|
||||
if (right_count > samples_count) {
|
||||
right_count = samples_count;
|
||||
}
|
||||
memcpy(to,
|
||||
buf->data + (tail * buf->sample_size),
|
||||
right_count * buf->sample_size);
|
||||
uint32_t right_count = buf->alloc_size - tail;
|
||||
if (right_count > samples_count) {
|
||||
right_count = samples_count;
|
||||
}
|
||||
memcpy(to,
|
||||
buf->data + (tail * buf->sample_size),
|
||||
right_count * buf->sample_size);
|
||||
|
||||
if (samples_count > right_count) {
|
||||
uint32_t left_count = samples_count - right_count;
|
||||
memcpy(to + (right_count * buf->sample_size),
|
||||
buf->data,
|
||||
left_count * buf->sample_size);
|
||||
}
|
||||
if (samples_count > right_count) {
|
||||
uint32_t left_count = samples_count - right_count;
|
||||
memcpy(to + (right_count * buf->sample_size),
|
||||
buf->data,
|
||||
left_count * buf->sample_size);
|
||||
}
|
||||
|
||||
uint32_t new_tail = (tail + samples_count) % buf->alloc_size;
|
||||
@ -86,6 +87,9 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
|
||||
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
|
||||
|
||||
uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size;
|
||||
if (!can_write) {
|
||||
return 0;
|
||||
}
|
||||
if (samples_count > can_write) {
|
||||
samples_count = can_write;
|
||||
}
|
||||
@ -110,3 +114,29 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
|
||||
|
||||
return samples_count;
|
||||
}
|
||||
|
||||
uint32_t
|
||||
sc_audiobuf_truncate(struct sc_audiobuf *buf, uint32_t samples_limit) {
|
||||
for (;;) {
|
||||
// Only the writer thread can write head, so memory_order_relaxed is
|
||||
// sufficient
|
||||
uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed);
|
||||
|
||||
// The tail cursor is updated after the data is consumed by the reader
|
||||
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
|
||||
|
||||
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
|
||||
if (can_read <= samples_limit) {
|
||||
// Nothing to truncate
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t skip = can_read - samples_limit;
|
||||
uint32_t new_tail = (tail + skip) % buf->alloc_size;
|
||||
if (atomic_compare_exchange_weak_explicit(&buf->tail, &tail, new_tail,
|
||||
memory_order_acq_rel,
|
||||
memory_order_acquire)) {
|
||||
return skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,16 @@ uint32_t
|
||||
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
|
||||
uint32_t samples_count);
|
||||
|
||||
/**
|
||||
* Drop old samples to keep at most sample_limit samples
|
||||
*
|
||||
* Must be called by the writer thread.
|
||||
*
|
||||
* \return the number of samples dropped
|
||||
*/
|
||||
uint32_t
|
||||
sc_audiobuf_truncate(struct sc_audiobuf *buf, uint32_t samples_limit);
|
||||
|
||||
static inline uint32_t
|
||||
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
|
||||
assert(buf->alloc_size);
|
||||
|
11
doc/audio.md
11
doc/audio.md
@ -28,10 +28,17 @@ To disable only the audio playback, see [no playback](video.md#no-playback).
|
||||
|
||||
## Audio only
|
||||
|
||||
To play audio only, disable the video:
|
||||
To play audio only, disable video and control:
|
||||
|
||||
```bash
|
||||
scrcpy --no-video
|
||||
scrcpy --no-video --no-control
|
||||
```
|
||||
|
||||
To play audio without a window:
|
||||
|
||||
```bash
|
||||
# --no-video and --no-control are implied by --no-window
|
||||
scrcpy --no-window
|
||||
# interrupt with Ctrl+C
|
||||
```
|
||||
|
||||
|
@ -15,6 +15,31 @@ scrcpy -n # short version
|
||||
Read [keyboard](keyboard.md) and [mouse](mouse.md).
|
||||
|
||||
|
||||
## Control only
|
||||
|
||||
To control the device without mirroring:
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio
|
||||
```
|
||||
|
||||
By default, mouse mode is switched to UHID if video mirroring is disabled (a
|
||||
relative mouse mode is required).
|
||||
|
||||
To also use a UHID keyboard, set it explicitly:
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --keyboard=uhid
|
||||
scrcpy --no-video --no-audio -K # short version
|
||||
```
|
||||
|
||||
To use AOA instead (over USB only):
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa
|
||||
```
|
||||
|
||||
|
||||
## Copy-paste
|
||||
|
||||
Any time the Android clipboard changes, it is automatically synchronized to the
|
||||
|
43
doc/otg.md
43
doc/otg.md
@ -1,19 +1,21 @@
|
||||
# OTG
|
||||
|
||||
By default, _scrcpy_ injects input events at the Android API level. As an
|
||||
alternative, when connected over USB, it is possible to send HID events, so that
|
||||
scrcpy behaves as if it was a physical keyboard and/or mouse connected to the
|
||||
Android device.
|
||||
alternative, it is possible to send HID events, so that scrcpy behaves as if it
|
||||
was a [physical keyboard] and/or a [physical mouse] connected to the Android
|
||||
device (see [keyboard](keyboard.md) and [mouse](mouse.md)).
|
||||
|
||||
A special mode allows to control the device without mirroring, using AOA
|
||||
[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible
|
||||
to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if
|
||||
the computer keyboard and mouse were plugged directly to the device via an OTG
|
||||
cable.
|
||||
[physical keyboard]: keyboard.md#physical-keyboard-simulation
|
||||
[physical mouse]: physical-keyboard-simulation
|
||||
|
||||
In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled.
|
||||
A special mode (OTG) allows to control the device using AOA
|
||||
[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at
|
||||
all (so USB debugging is not necessary). In this mode, video and audio are
|
||||
disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set.
|
||||
|
||||
This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring.
|
||||
Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse
|
||||
simulation, as if the computer keyboard and mouse were plugged directly to the
|
||||
device via an OTG cable.
|
||||
|
||||
To enable OTG mode:
|
||||
|
||||
@ -23,7 +25,7 @@ scrcpy --otg
|
||||
scrcpy --otg -s 0123456789abcdef
|
||||
```
|
||||
|
||||
It is possible to disable HID keyboard or HID mouse:
|
||||
It is possible to disable keyboard or mouse:
|
||||
|
||||
```bash
|
||||
scrcpy --otg --keyboard=disabled
|
||||
@ -35,3 +37,22 @@ It only works if the device is connected over USB.
|
||||
## OTG issues on Windows
|
||||
|
||||
See [FAQ](/FAQ.md#otg-issues-on-windows).
|
||||
|
||||
|
||||
## Control only
|
||||
|
||||
Note that the purpose of OTG is to control the device without USB debugging
|
||||
(adb).
|
||||
|
||||
If you want to solely control the device without mirroring while USB debugging
|
||||
is enabled, then OTG mode is not necessary.
|
||||
|
||||
Instead, disable video and audio, and select UHID (or AOA):
|
||||
|
||||
```bash
|
||||
scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid
|
||||
scrcpy --no-video --no-audio -KM # short version
|
||||
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa
|
||||
```
|
||||
|
||||
One benefit of UHID is that it also works wirelessly.
|
||||
|
@ -58,12 +58,10 @@ orientation](video.md#orientation).
|
||||
|
||||
## No playback
|
||||
|
||||
To disable playback while recording:
|
||||
To disable playback and control while recording:
|
||||
|
||||
```bash
|
||||
scrcpy --no-playback --record=file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# interrupt recording with Ctrl+C
|
||||
scrcpy --no-playback --no-control --record=file.mp4
|
||||
```
|
||||
|
||||
It is also possible to disable video and audio playback separately:
|
||||
@ -73,6 +71,13 @@ It is also possible to disable video and audio playback separately:
|
||||
scrcpy --record=file.mkv --no-audio-playback
|
||||
```
|
||||
|
||||
To also disable the window:
|
||||
|
||||
```bash
|
||||
scrcpy --no-playback --no-window --record=file.mp4
|
||||
# interrupt recording with Ctrl+C
|
||||
```
|
||||
|
||||
## Time limit
|
||||
|
||||
To limit the recording time:
|
||||
|
@ -28,6 +28,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
|
||||
| Flip display horizontally | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>←</kbd> _(left)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>→</kbd> _(right)_
|
||||
| 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>
|
||||
| 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_
|
||||
|
@ -1,5 +1,14 @@
|
||||
# Window
|
||||
|
||||
## Disable window
|
||||
|
||||
To disable window (may be useful for recording or for playing audio only):
|
||||
|
||||
```bash
|
||||
scrcpy --no-window --record=file.mp4
|
||||
# Ctrl+C to interrupt
|
||||
```
|
||||
|
||||
## Title
|
||||
|
||||
By default, the window title is the device model. It can be changed:
|
||||
|
@ -127,6 +127,10 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
||||
android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class);
|
||||
if (sizes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Stream<android.util.Size> stream = Arrays.stream(sizes);
|
||||
if (maxSize > 0) {
|
||||
stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize);
|
||||
|
@ -118,12 +118,16 @@ public final class LogUtils {
|
||||
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
||||
|
||||
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
|
||||
for (android.util.Size size : sizes) {
|
||||
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
|
||||
if (sizes == null || sizes.length == 0) {
|
||||
builder.append("\n (none)");
|
||||
} else {
|
||||
for (android.util.Size size : sizes) {
|
||||
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes();
|
||||
if (highSpeedSizes.length > 0) {
|
||||
if (highSpeedSizes != null && highSpeedSizes.length > 0) {
|
||||
builder.append("\n High speed capture (--camera-high-speed):");
|
||||
for (android.util.Size size : highSpeedSizes) {
|
||||
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
|
||||
|
@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Surface;
|
||||
@ -47,7 +48,7 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
this.downsizeOnError = downsizeOnError;
|
||||
}
|
||||
|
||||
private void streamScreen() throws IOException, ConfigurationException {
|
||||
private void streamCapture() throws IOException, ConfigurationException {
|
||||
Codec codec = streamer.getCodec();
|
||||
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
|
||||
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
|
||||
@ -220,6 +221,9 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
|
||||
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED);
|
||||
}
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
|
||||
// display the very first frame, and recover from bad quality when no new frames
|
||||
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
|
||||
@ -250,7 +254,7 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
Looper.prepare();
|
||||
|
||||
try {
|
||||
streamScreen();
|
||||
streamCapture();
|
||||
} catch (ConfigurationException e) {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
} catch (IOException e) {
|
||||
|
@ -12,12 +12,15 @@ import java.lang.reflect.Method;
|
||||
public final class WindowManager {
|
||||
private final IInterface manager;
|
||||
private Method getRotationMethod;
|
||||
private Method freezeRotationMethod;
|
||||
|
||||
private Method freezeDisplayRotationMethod;
|
||||
private Method isRotationFrozenMethod;
|
||||
private int freezeDisplayRotationMethodVersion;
|
||||
|
||||
private Method isDisplayRotationFrozenMethod;
|
||||
private Method thawRotationMethod;
|
||||
private int isDisplayRotationFrozenMethodVersion;
|
||||
|
||||
private Method thawDisplayRotationMethod;
|
||||
private int thawDisplayRotationMethodVersion;
|
||||
|
||||
static WindowManager create() {
|
||||
IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager");
|
||||
@ -43,50 +46,61 @@ public final class WindowManager {
|
||||
return getRotationMethod;
|
||||
}
|
||||
|
||||
private Method getFreezeRotationMethod() throws NoSuchMethodException {
|
||||
if (freezeRotationMethod == null) {
|
||||
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
|
||||
}
|
||||
return freezeRotationMethod;
|
||||
}
|
||||
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
private Method getFreezeDisplayRotationMethod() throws NoSuchMethodException {
|
||||
if (freezeDisplayRotationMethod == null) {
|
||||
freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class);
|
||||
try {
|
||||
// Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/670fb7f5c0d23cf51ead25538bcb017e03ed73ac%5E%21/>
|
||||
freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class, String.class);
|
||||
freezeDisplayRotationMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e) {
|
||||
try {
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
freezeDisplayRotationMethod = manager.getClass().getMethod("freezeDisplayRotation", int.class, int.class);
|
||||
freezeDisplayRotationMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
freezeDisplayRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
|
||||
freezeDisplayRotationMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return freezeDisplayRotationMethod;
|
||||
}
|
||||
|
||||
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
|
||||
if (isRotationFrozenMethod == null) {
|
||||
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
|
||||
}
|
||||
return isRotationFrozenMethod;
|
||||
}
|
||||
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
private Method getIsDisplayRotationFrozenMethod() throws NoSuchMethodException {
|
||||
if (isDisplayRotationFrozenMethod == null) {
|
||||
isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class);
|
||||
try {
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
isDisplayRotationFrozenMethod = manager.getClass().getMethod("isDisplayRotationFrozen", int.class);
|
||||
isDisplayRotationFrozenMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e) {
|
||||
isDisplayRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
|
||||
isDisplayRotationFrozenMethodVersion = 1;
|
||||
}
|
||||
}
|
||||
return isDisplayRotationFrozenMethod;
|
||||
}
|
||||
|
||||
private Method getThawRotationMethod() throws NoSuchMethodException {
|
||||
if (thawRotationMethod == null) {
|
||||
thawRotationMethod = manager.getClass().getMethod("thawRotation");
|
||||
}
|
||||
return thawRotationMethod;
|
||||
}
|
||||
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
private Method getThawDisplayRotationMethod() throws NoSuchMethodException {
|
||||
if (thawDisplayRotationMethod == null) {
|
||||
thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class);
|
||||
try {
|
||||
// Android 15 preview and 14 QPR3 Beta added a String caller parameter for debugging:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/670fb7f5c0d23cf51ead25538bcb017e03ed73ac%5E%21/>
|
||||
thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class, String.class);
|
||||
thawDisplayRotationMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e) {
|
||||
try {
|
||||
// New method added by this commit:
|
||||
// <https://android.googlesource.com/platform/frameworks/base/+/90c9005e687aa0f63f1ac391adc1e8878ab31759%5E%21/>
|
||||
thawDisplayRotationMethod = manager.getClass().getMethod("thawDisplayRotation", int.class);
|
||||
thawDisplayRotationMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
thawDisplayRotationMethod = manager.getClass().getMethod("thawRotation");
|
||||
thawDisplayRotationMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return thawDisplayRotationMethod;
|
||||
}
|
||||
@ -103,16 +117,21 @@ public final class WindowManager {
|
||||
|
||||
public void freezeRotation(int displayId, int rotation) {
|
||||
try {
|
||||
try {
|
||||
Method method = getFreezeDisplayRotationMethod();
|
||||
method.invoke(manager, displayId, rotation);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
if (displayId == 0) {
|
||||
Method method = getFreezeRotationMethod();
|
||||
Method method = getFreezeDisplayRotationMethod();
|
||||
switch (freezeDisplayRotationMethodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, displayId, rotation, "scrcpy#freezeRotation");
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, displayId, rotation);
|
||||
break;
|
||||
default:
|
||||
if (displayId != 0) {
|
||||
Ln.e("Secondary display rotation not supported on this device");
|
||||
return;
|
||||
}
|
||||
method.invoke(manager, rotation);
|
||||
} else {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
@ -121,17 +140,16 @@ public final class WindowManager {
|
||||
|
||||
public boolean isRotationFrozen(int displayId) {
|
||||
try {
|
||||
try {
|
||||
Method method = getIsDisplayRotationFrozenMethod();
|
||||
return (boolean) method.invoke(manager, displayId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
if (displayId == 0) {
|
||||
Method method = getIsRotationFrozenMethod();
|
||||
Method method = getIsDisplayRotationFrozenMethod();
|
||||
switch (isDisplayRotationFrozenMethodVersion) {
|
||||
case 0:
|
||||
return (boolean) method.invoke(manager, displayId);
|
||||
default:
|
||||
if (displayId != 0) {
|
||||
Ln.e("Secondary display rotation not supported on this device");
|
||||
return false;
|
||||
}
|
||||
return (boolean) method.invoke(manager);
|
||||
} else {
|
||||
Ln.e("Could not invoke method", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
@ -141,16 +159,21 @@ public final class WindowManager {
|
||||
|
||||
public void thawRotation(int displayId) {
|
||||
try {
|
||||
try {
|
||||
Method method = getThawDisplayRotationMethod();
|
||||
method.invoke(manager, displayId);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
if (displayId == 0) {
|
||||
Method method = getThawRotationMethod();
|
||||
Method method = getThawDisplayRotationMethod();
|
||||
switch (thawDisplayRotationMethodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, displayId, "scrcpy#thawRotation");
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, displayId);
|
||||
break;
|
||||
default:
|
||||
if (displayId != 0) {
|
||||
Ln.e("Secondary display rotation not supported on this device");
|
||||
return;
|
||||
}
|
||||
method.invoke(manager);
|
||||
} else {
|
||||
Ln.e("Could not invoke method", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
@ -166,6 +189,10 @@ public final class WindowManager {
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// old version
|
||||
if (displayId != 0) {
|
||||
Ln.e("Secondary display rotation not supported on this device");
|
||||
return;
|
||||
}
|
||||
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
Reference in New Issue
Block a user