Compare commits

..

14 Commits

Author SHA1 Message Date
Romain Vimont
ba86fefdba Document camera torch and zoom features 2026-01-10 16:30:09 +01:00
Tommie
b3fd4b0de3 Add shortcuts to change the camera zoom
MOD+up and MOD+zoom change the camera zoom.

TODO ref 6243

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-10 16:29:39 +01:00
Tommie
90ce7fbcb0 Add option to specify the camera zoom
Add --camera-zoom to specify the camera zoom.

TODO ref 6243.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-10 16:24:56 +01:00
Romain Vimont
bae50c32f9 Display fps set with '{}'
Replace "fps=[15, 30, 60]" with "fps={15, 30, 60}".

The default toString() implementation for a SortedSet uses '[]', but
it is more correct to use '{}' to denote a set.
2026-01-10 16:24:56 +01:00
Romain Vimont
5381e7837c Add shortcuts to switch the camera torch
MOD+t turns on the camera torch, MOD+Shift+t turns it off.

TODO ref 6243

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-10 16:24:54 +01:00
Tommie
6919628705 Add option to turn on the camera torch
Add --camera-torch to turn on the camera torch when the camera starts.

TODO ref 6243.

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-10 11:53:22 +01:00
Romain Vimont
17a368d982 Simplify camera startup code
Avoid multiple back-and-forths between the caller thread and the camera
thread.
2026-01-10 11:53:22 +01:00
Romain Vimont
3d4a1a50b9 Enable "reset video" shortcut for camera
Make the existing "reset video" feature (MOD+Shift+R) also work for a
camera video source.
2026-01-10 11:53:22 +01:00
Romain Vimont
bc75bf09e9 Enable controls for camera video source
This will allow the implementation of camera-specific shortcuts.

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-10 11:53:22 +01:00
Romain Vimont
52658eb2d6 Report control protocol errors
All IOExceptions were ignored to avoid an error on close, but protocol
exceptions must be reported.
2026-01-10 11:53:22 +01:00
Romain Vimont
3f06378164 Throw error on unexpected control message type 2026-01-10 11:53:22 +01:00
Romain Vimont
e1681d2d37 Group control event shortcuts
Group together the shortcuts that trigger control events to be sent to
the device.
2026-01-10 11:53:22 +01:00
Romain Vimont
599108bf90 Simplify capture invalidation
Remove the unnecessary requestInvalidate() indirection. Use a single
invalidate() method instead.
2026-01-10 11:53:22 +01:00
Romain Vimont
8967a0d59d Move precondition checks for input events
Always call the appropriate method responsible for handling the input
event, which can then decide to do nothing.
2026-01-10 11:53:22 +01:00
54 changed files with 1298 additions and 1410 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,2 @@
@echo off
scrcpy.exe --pause-on-exit=if-error %*

View File

@@ -15,7 +15,7 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/disconnect.c',
'src/display.c',
'src/events.c',
'src/icon.c',
'src/file_pusher.c',
@@ -33,7 +33,6 @@ src = [
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/texture.c',
'src/version.c',
'src/hid/hid_gamepad.c',
'src/hid/hid_keyboard.c',
@@ -76,7 +75,6 @@ conf.set('_GNU_SOURCE', true)
if host_machine.system() == 'windows'
windows = import('windows')
src += [
'src/util/command.c',
'src/sys/win/file.c',
'src/sys/win/process.c',
windows.compile_resources('scrcpy-windows.rc'),
@@ -106,6 +104,7 @@ if usb_support
'src/usb/keyboard_aoa.c',
'src/usb/mouse_aoa.c',
'src/usb/scrcpy_otg.c',
'src/usb/screen_otg.c',
'src/usb/usb.c',
]
endif
@@ -192,7 +191,8 @@ executable('scrcpy', src,
datadir = get_option('datadir') # by default 'share'
install_man('scrcpy.1')
install_data('data/scrcpy.png',
install_data('data/icon.png',
rename: 'scrcpy.png',
install_dir: datadir / 'icons/hicolor/256x256/apps')
install_data('data/zsh-completion/_scrcpy',
install_dir: datadir / 'zsh/site-functions')
@@ -239,12 +239,6 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c',
'src/util/term.c',
]],
['test_command_windows', [
'tests/test_command_windows.c',
'src/util/command.c',
'src/util/str.c',
'src/util/strbuf.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
@@ -289,6 +283,6 @@ endif
if meson.version().version_compare('>= 0.58.0')
devenv = environment()
devenv.set('SCRCPY_ICON_DIR', meson.current_source_dir() / 'data')
devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png')
meson.add_devenv(devenv)
endif

View File

@@ -851,8 +851,8 @@ Path to adb.
Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
.TP
.B SCRCPY_ICON_DIR
Path to the icon directory.
.B SCRCPY_ICON_PATH
Path to the program icon.
.TP
.B SCRCPY_SERVER_PATH

View File

@@ -331,24 +331,56 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
bool
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags) {
#ifdef _WIN32
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
if (!local) {
return SC_PROCESS_NONE;
}
remote = sc_str_quote(remote);
if (!remote) {
free((void *) local);
return SC_PROCESS_NONE;
}
#endif
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "push", local, remote);
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32
free((void *) remote);
free((void *) local);
#endif
return process_check_success_intr(intr, pid, "adb push", flags);
}
bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) {
#ifdef _WIN32
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
if (!local) {
return SC_PROCESS_NONE;
}
#endif
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "install", "-r", local);
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32
free((void *) local);
#endif
return process_check_success_intr(intr, pid, "adb install", flags);
}

View File

@@ -39,11 +39,5 @@ sc_adb_device_get_type(const char *serial) {
return SC_ADB_DEVICE_TYPE_TCPIP;
}
// TCP/IP devices provided by mDNS contain "adb-tls-connect"
// <https://github.com/Genymobile/scrcpy/issues/6248>
if (strstr(serial, "adb-tls-connect")) {
return SC_ADB_DEVICE_TYPE_TCPIP;
}
return SC_ADB_DEVICE_TYPE_USB;
}

View File

@@ -8,49 +8,6 @@
#include "util/log.h"
#include "util/str.h"
static size_t
rstrip_len(const char *s, size_t len) {
size_t i = len;
// Ignore trailing whitespaces
while (i > 0 && (s[i-1] == ' ' || s[i-1] == '\t')) {
--i;
}
return i;
}
static void
locate_last_token(const char *s, size_t len, size_t *start, size_t *end) {
size_t i = rstrip_len(s, len);
*end = i; // excluded
// The token contains non-whitespace chars
while (i > 0 && (s[i-1] != ' ' && s[i-1] != '\t')) {
--i;
}
*start = i; // included
}
static bool
is_device_state(const char *s) {
// <https://android.googlesource.com/platform/packages/modules/adb/+/1cf2f017d312f73b3dc53bda85ef2610e35a80e9/adb.cpp#144>
// "device", "unauthorized" and "offline" are the most common states, so
// check them first.
return !strcmp(s, "device")
|| !strcmp(s, "unauthorized")
|| !strcmp(s, "offline")
|| !strcmp(s, "bootloader")
|| !strcmp(s, "host")
|| !strcmp(s, "recovery")
|| !strcmp(s, "rescue")
|| !strcmp(s, "sideload")
|| !strcmp(s, "authorizing")
|| !strcmp(s, "connecting")
|| !strcmp(s, "detached");
}
static bool
sc_adb_parse_device(char *line, struct sc_adb_device *device) {
// One device line looks like:
@@ -68,54 +25,64 @@ sc_adb_parse_device(char *line, struct sc_adb_device *device) {
return false;
}
size_t len = strlen(line);
char *s = line; // cursor in the line
size_t start;
size_t end;
// The serial (the first token) may contain spaces, which are also token
// separators. To avoid ambiguity, parse the string backwards:
// - first, parse all the trailing values until the device state,
// identified using a list of well-known values;
// - finally, treat the remaining leading token as the device serial.
//
// Refs:
// - <https://github.com/Genymobile/scrcpy/issues/6248>
// - <https://github.com/Genymobile/scrcpy/issues/3537>
const char *state;
const char *model = NULL;
for (;;) {
locate_last_token(line, len, &start, &end);
if (start == end) {
// No more tokens, unexpected
return false;
}
const char *token = &line[start];
line[end] = '\0';
if (!strncmp("model:", token, sizeof("model:") - 1)) {
model = &token[sizeof("model:") - 1];
// We only need the model
} else if (is_device_state(token)) {
state = token;
// The device state is the item immediately after the device serial
break;
}
// Remove the trailing parts already handled
len = start;
}
assert(state);
size_t serial_len = rstrip_len(line, start);
// After the serial:
// - "adb devices" writes a single '\t'
// - "adb devices -l" writes multiple spaces
// For flexibility, accept both.
size_t serial_len = strcspn(s, " \t");
if (!serial_len) {
// empty serial
return false;
}
char *serial = line;
line[serial_len] = '\0';
bool eol = s[serial_len] == '\0';
if (eol) {
// serial alone is unexpected
return false;
}
s[serial_len] = '\0';
char *serial = s;
s += serial_len + 1;
// After the serial, there might be several spaces
s += strspn(s, " \t"); // consume all separators
size_t state_len = strcspn(s, " ");
if (!state_len) {
// empty state
return false;
}
eol = s[state_len] == '\0';
s[state_len] = '\0';
char *state = s;
char *model = NULL;
if (!eol) {
s += state_len + 1;
// Iterate over all properties "key:value key:value ..."
for (;;) {
size_t token_len = strcspn(s, " ");
if (!token_len) {
break;
}
eol = s[token_len] == '\0';
s[token_len] = '\0';
char *token = s;
if (!strncmp("model:", token, sizeof("model:") - 1)) {
model = &token[sizeof("model:") - 1];
// We only need the model
break;
}
if (eol) {
break;
} else {
s+= token_len + 1;
}
}
}
device->serial = strdup(serial);
if (!device->serial) {

View File

@@ -120,6 +120,14 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
ap->device = SDL_GetAudioStreamDevice(ap->stream);
assert(ap->device);
// The thread calling open() is the thread calling push(), which fills the
// audio buffer consumed by the SDL audio thread.
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
if (!ok) {
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH);
(void) ok; // We don't care if it worked, at least we tried
}
ok = SDL_ResumeAudioDevice(ap->device);
if (!ok) {
LOGE("Could not resume audio device: %s", SDL_GetError());

View File

@@ -1249,8 +1249,8 @@ static const struct sc_envvar envvars[] = {
"--tcpip=<addr>) is specified",
},
{
.name = "SCRCPY_ICON_DIR",
.text = "Path to the icon directory",
.name = "SCRCPY_ICON_PATH",
.text = "Path to the program icon",
},
{
.name = "SCRCPY_SERVER_PATH",
@@ -1729,7 +1729,7 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return true;
}
LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, "
"flip90, flip180 or flip270)", s);
"flip90, flip180 or flip270)", optarg);
return false;
}
@@ -3371,7 +3371,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
}
if (arg[15] != '=') {
// Invalid parameter, ignore
return SC_PAUSE_ON_EXIT_UNDEFINED;
return SC_PAUSE_ON_EXIT_FALSE;
}
const char *value = &arg[16];
if (!strcmp(value, "true")) {
@@ -3380,44 +3380,14 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
if (!strcmp(value, "false")) {
return SC_PAUSE_ON_EXIT_FALSE;
}
return SC_PAUSE_ON_EXIT_UNDEFINED;
// Set to false, including when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
}
}
return SC_PAUSE_ON_EXIT_UNDEFINED;
return SC_PAUSE_ON_EXIT_FALSE;
}
#ifdef _WIN32
/**
* Attempt to detect whether the user launched scrcpy by double-clicking
* scrcpy.exe in Windows Explorer.
*
* If so, the console should remain open on error.
*/
static bool
scrcpy_launched_by_double_click(void) {
// No console window
if (GetConsoleWindow() == NULL) {
return false;
}
// Must be interactive
if (!_isatty(_fileno(stdin)) || !_isatty(_fileno(stdout))) {
return false;
}
// Check how many processes share the console
DWORD dummy;
DWORD count = GetConsoleProcessList(&dummy, 1);
// Only this process attached, assume it was started by double-clicking
return count == 1;
}
#endif
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
struct sc_getopt_adapter adapter;
@@ -3431,22 +3401,11 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
sc_getopt_adapter_destroy(&adapter);
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) {
// Check if "--pause-on-exit" is present in the arguments list, because
// it must be taken into account even if command line parsing failed
args->pause_on_exit = sc_get_pause_on_exit(argc, argv);
}
if (args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_FALSE;
#ifdef _WIN32
if (scrcpy_launched_by_double_click()) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR;
}
#endif
}
assert(args->pause_on_exit != SC_PAUSE_ON_EXIT_UNDEFINED);
return ret;
}

View File

@@ -8,7 +8,6 @@
#include "options.h"
enum sc_pause_on_exit {
SC_PAUSE_ON_EXIT_UNDEFINED,
SC_PAUSE_ON_EXIT_TRUE,
SC_PAUSE_ON_EXIT_FALSE,
SC_PAUSE_ON_EXIT_IF_ERROR,

View File

@@ -1,88 +0,0 @@
#include "disconnect.h"
#include <assert.h>
#include "icon.h"
#include "util/log.h"
static int
run(void *userdata) {
struct sc_disconnect *d = userdata;
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_DISCONNECTED);
if (icon) {
d->cbs->on_icon_loaded(d, icon, d->cbs_userdata);
} else {
LOGE("Could not load disconnected icon");
}
sc_mutex_lock(&d->mutex);
bool timed_out = false;
while (!d->interrupted && !timed_out) {
timed_out = !sc_cond_timedwait(&d->cond, &d->mutex, d->deadline);
}
bool interrupted = d->interrupted;
sc_mutex_unlock(&d->mutex);
if (!interrupted) {
d->cbs->on_timeout(d, d->cbs_userdata);
}
return 0;
}
bool
sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline,
const struct sc_disconnect_callbacks *cbs,
void *cbs_userdata) {
bool ok = sc_mutex_init(&d->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&d->cond);
if (!ok) {
goto error_destroy_mutex;
}
d->deadline = deadline;
d->interrupted = false;
assert(cbs && cbs->on_icon_loaded && cbs->on_timeout);
d->cbs = cbs;
d->cbs_userdata = cbs_userdata;
ok = sc_thread_create(&d->thread, run, "scrcpy-dis", d);
if (!ok) {
goto error_destroy_cond;
}
return true;
error_destroy_cond:
sc_cond_destroy(&d->cond);
error_destroy_mutex:
sc_mutex_destroy(&d->mutex);
return false;
}
void
sc_disconnect_interrupt(struct sc_disconnect *d) {
sc_mutex_lock(&d->mutex);
d->interrupted = true;
sc_mutex_unlock(&d->mutex);
// wake up blocking wait
sc_cond_signal(&d->cond);
}
void
sc_disconnect_join(struct sc_disconnect *d) {
sc_thread_join(&d->thread, NULL);
}
void
sc_disconnect_destroy(struct sc_disconnect *d) {
sc_cond_destroy(&d->cond);
sc_mutex_destroy(&d->mutex);
}

View File

@@ -1,47 +0,0 @@
#ifndef SC_DISCONNECT
#define SC_DISCONNECT
#include "common.h"
#include "SDL3/SDL_surface.h"
#include "util/tick.h"
#include "util/thread.h"
// Tool to handle loading the icon and signal timeout when the device is
// unexpectedly disconnected
struct sc_disconnect {
sc_tick deadline;
struct sc_thread thread;
struct sc_mutex mutex;
struct sc_cond cond;
bool interrupted;
const struct sc_disconnect_callbacks *cbs;
void *cbs_userdata;
};
struct sc_disconnect_callbacks {
// Called when the disconnected icon is loaded
void (*on_icon_loaded)(struct sc_disconnect *d, SDL_Surface *icon,
void *userdata);
// Called when the timeout expired (the scrcpy window must be closed)
void (*on_timeout)(struct sc_disconnect *d, void *userdata);
};
bool
sc_disconnect_start(struct sc_disconnect *d, sc_tick deadline,
const struct sc_disconnect_callbacks *cbs,
void *cbs_userdata);
void
sc_disconnect_interrupt(struct sc_disconnect *d);
void
sc_disconnect_join(struct sc_disconnect *d);
void
sc_disconnect_destroy(struct sc_disconnect *d);
#endif

429
app/src/display.c Normal file
View File

@@ -0,0 +1,429 @@
#include "display.h"
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <libavutil/pixfmt.h>
#include "util/log.h"
#include "util/sdl.h"
static bool
sc_display_init_novideo_icon(struct sc_display *display,
SDL_Surface *icon_novideo) {
assert(icon_novideo);
bool ok = SDL_SetRenderLogicalPresentation(display->renderer,
icon_novideo->w,
icon_novideo->h,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (!ok) {
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,
SDL_Surface *icon_novideo, bool mipmaps) {
display->renderer = SDL_CreateRenderer(window, NULL);
if (!display->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
return false;
}
const char *renderer_name = SDL_GetRendererName(display->renderer);
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
display->mipmaps = false;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
display->gl_context = NULL;
#endif
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
display->gl_context = SDL_GL_CreateContext(window);
if (!display->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
SDL_DestroyRenderer(display->renderer);
return false;
}
#endif
struct sc_opengl *gl = &display->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
display->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
display->texture = NULL;
display->pending.flags = 0;
display->pending.frame = NULL;
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_DestroyContext(display->gl_context);
#endif
SDL_DestroyRenderer(display->renderer);
return false;
}
}
return true;
}
void
sc_display_destroy(struct sc_display *display) {
if (display->pending.frame) {
av_frame_free(&display->pending.frame);
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(display->gl_context);
#endif
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
SDL_DestroyRenderer(display->renderer);
}
static enum SDL_Colorspace
sc_display_to_sdl_color_space(enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool full_range = color_range == AVCOL_RANGE_JPEG;
switch (color_space) {
case AVCOL_SPC_BT709:
case AVCOL_SPC_RGB:
return full_range ? SDL_COLORSPACE_BT709_FULL
: SDL_COLORSPACE_BT709_LIMITED;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
return full_range ? SDL_COLORSPACE_BT601_FULL
: SDL_COLORSPACE_BT601_LIMITED;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
return full_range ? SDL_COLORSPACE_BT2020_FULL
: SDL_COLORSPACE_BT2020_LIMITED;
default:
return SDL_COLORSPACE_JPEG;
}
}
static SDL_Texture *
sc_display_create_texture(struct sc_display *display,
struct sc_size size, enum AVColorSpace color_space,
enum AVColorRange color_range) {
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
enum SDL_Colorspace sdl_color_space =
sc_display_to_sdl_color_space(color_space, color_range);
bool ok =
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_YV12);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
SDL_TEXTUREACCESS_STREAMING);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
size.width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
size.height);
ok &= SDL_SetNumberProperty(props,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
sdl_color_space);
if (!ok) {
LOGE("Could not set texture properties");
SDL_DestroyProperties(props);
return NULL;
}
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
SDL_DestroyProperties(props);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (display->mipmaps) {
struct sc_opengl *gl = &display->gl;
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
if (!props) {
LOGE("Could not get texture properties: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
const char *renderer_name = SDL_GetRendererName(display->renderer);
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
SDL_DestroyProperties(props);
if (!texture_id) {
LOGE("Could not get texture id: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
display->texture_id = texture_id;
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return texture;
}
static inline void
sc_display_set_pending_texture(struct sc_display *display,
struct sc_size size,
enum AVColorRange color_range) {
assert(!display->texture);
display->pending.texture.size = size;
display->pending.texture.color_range = color_range;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_TEXTURE;
}
static bool
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
if (!display->pending.frame) {
display->pending.frame = av_frame_alloc();
if (!display->pending.frame) {
LOG_OOM();
return false;
}
}
av_frame_unref(display->pending.frame);
int r = av_frame_ref(display->pending.frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
return false;
}
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
return true;
}
// Forward declaration
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame);
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_TEXTURE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display,
display->pending.texture.size,
display->pending.texture.color_space,
display->pending.texture.color_range);
if (!display->texture) {
return false;
}
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_TEXTURE;
}
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
bool ok = sc_display_update_texture_internal(display,
display->pending.frame);
if (!ok) {
return false;
}
av_frame_unref(display->pending.frame);
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
}
return true;
}
static bool
sc_display_prepare_texture_internal(struct sc_display *display,
struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
assert(size.width && size.height);
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
display->texture =
sc_display_create_texture(display, size, color_space, color_range);
if (!display->texture) {
return false;
}
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
return true;
}
enum sc_display_result
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool ok = sc_display_prepare_texture_internal(display, size, color_space,
color_range);
if (!ok) {
sc_display_set_pending_texture(display, size, color_range);
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
bool ok = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (!ok) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (display->mipmaps) {
assert(display->texture_id);
struct sc_opengl *gl = &display->gl;
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
gl->GenerateMipmap(GL_TEXTURE_2D);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return true;
}
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
bool ok = sc_display_update_texture_internal(display, frame);
if (!ok) {
ok = sc_display_set_pending_frame(display, frame);
if (!ok) {
LOGE("Could not set pending frame");
return SC_DISPLAY_RESULT_ERROR;
}
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation) {
sc_sdl_render_clear(display->renderer);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
if (!ok) {
return SC_DISPLAY_RESULT_PENDING;
}
}
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture;
if (orientation == SC_ORIENTATION_0) {
SDL_FRect frect;
SDL_FRect *fgeometry = NULL;
if (geometry) {
SDL_RectToFRect(geometry, &frect);
fgeometry = &frect;
}
bool ok = SDL_RenderTexture(renderer, texture, NULL, fgeometry);
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
SDL_FRect frect;
if (sc_orientation_is_swap(orientation)) {
frect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
frect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
frect.w = geometry->h;
frect.h = geometry->w;
} else {
SDL_RectToFRect(geometry, &frect);
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
bool ok = SDL_RenderTextureRotated(renderer, texture, NULL, &frect,
angle, NULL, flip);
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
}
sc_sdl_render_present(display->renderer);
return SC_DISPLAY_RESULT_OK;
}

69
app/src/display.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavutil/frame.h>
#include <SDL3/SDL.h>
#include "coords.h"
#include "opengl.h"
#include "options.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_display {
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
bool mipmaps;
uint32_t texture_id; // only set if mipmaps is enabled
struct {
#define SC_DISPLAY_PENDING_FLAG_TEXTURE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct {
struct sc_size size;
enum AVColorSpace color_space;
enum AVColorRange color_range;
} texture;
AVFrame *frame;
} pending;
};
enum sc_display_result {
SC_DISPLAY_RESULT_OK,
SC_DISPLAY_RESULT_PENDING,
SC_DISPLAY_RESULT_ERROR,
};
bool
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
enum sc_display_result
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range);
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation);
#endif

View File

@@ -6,13 +6,9 @@
#include "util/thread.h"
bool
sc_push_event_impl(uint32_t type, void *ptr, const char *name) {
SDL_Event event = {
.user = {
.type = type,
.data1 = ptr,
}
};
sc_push_event_impl(uint32_t type, const char *name) {
SDL_Event event;
event.type = type;
bool ok = SDL_PushEvent(&event);
if (!ok) {
LOGE("Could not post %s event: %s", name, SDL_GetError());

View File

@@ -9,25 +9,22 @@
enum {
SC_EVENT_NEW_FRAME = SDL_EVENT_USER,
SC_EVENT_OPEN_WINDOW,
SC_EVENT_RUN_ON_MAIN_THREAD,
SC_EVENT_DEVICE_DISCONNECTED,
SC_EVENT_SERVER_CONNECTION_FAILED,
SC_EVENT_SERVER_CONNECTED,
SC_EVENT_USB_DEVICE_DISCONNECTED,
SC_EVENT_DEMUXER_ERROR,
SC_EVENT_RECORDER_ERROR,
SC_EVENT_TIME_LIMIT_REACHED,
SC_EVENT_CONTROLLER_ERROR,
SC_EVENT_AOA_OPEN_ERROR,
SC_EVENT_DISCONNECTED_ICON_LOADED,
SC_EVENT_DISCONNECTED_TIMEOUT,
};
bool
sc_push_event_impl(uint32_t type, void *ptr, const char *name);
sc_push_event_impl(uint32_t type, const char *name);
#define sc_push_event(TYPE) sc_push_event_impl(TYPE, NULL, # TYPE)
#define sc_push_event_with_data(TYPE, PTR) sc_push_event_impl(TYPE, PTR, # TYPE)
#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE)
typedef void (*sc_runnable_fn)(void *userdata);

View File

@@ -14,37 +14,33 @@
#include "config.h"
#include "util/env.h"
#include "util/file.h"
#ifdef PORTABLE
# include "util/file.h"
#endif
#include "util/log.h"
#define SCRCPY_DEFAULT_ICON_DIR PREFIX "/share/icons/hicolor/256x256/apps"
#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png"
#define SCRCPY_DEFAULT_ICON_PATH \
PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png"
static char *
get_icon_path(const char *filename) {
char *icon_path;
char *icon_dir = sc_get_env("SCRCPY_ICON_DIR");
if (icon_dir) {
get_icon_path(void) {
char *icon_path = sc_get_env("SCRCPY_ICON_PATH");
if (icon_path) {
// if the envvar is set, use it
icon_path = sc_file_build_path(icon_dir, filename);
free(icon_dir);
if (!icon_path) {
LOG_OOM();
return NULL;
}
LOGD("Using icon from SCRCPY_ICON_DIR: %s", icon_path);
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
return icon_path;
}
#ifndef PORTABLE
icon_path = sc_file_build_path(SCRCPY_DEFAULT_ICON_DIR, filename);
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
if (!icon_path) {
LOG_OOM();
return NULL;
}
LOGD("Using icon: %s", icon_path);
#else
icon_path = sc_file_get_local_path(filename);
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
if (!icon_path) {
LOGE("Could not get icon path");
return NULL;
@@ -181,7 +177,7 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) {
}
static SDL_Surface *
sc_icon_load_from_full_path(const char *path) {
load_from_path(const char *path) {
AVFrame *frame = decode_image(path);
if (!frame) {
return NULL;
@@ -278,19 +274,19 @@ error:
}
SDL_Surface *
sc_icon_load(const char *filename) {
char *icon_path = get_icon_path(filename);
scrcpy_icon_load(void) {
char *icon_path = get_icon_path();
if (!icon_path) {
return NULL;
}
SDL_Surface *icon = sc_icon_load_from_full_path(icon_path);
SDL_Surface *icon = load_from_path(icon_path);
free(icon_path);
return icon;
}
void
sc_icon_destroy(SDL_Surface *icon) {
scrcpy_icon_destroy(SDL_Surface *icon) {
SDL_PropertiesID props = SDL_GetSurfaceProperties(icon);
assert(props);
AVFrame *frame = SDL_GetPointerProperty(props, "sc_frame", NULL);

View File

@@ -5,13 +5,10 @@
#include <SDL3/SDL_surface.h>
#define SC_ICON_FILENAME_SCRCPY "scrcpy.png"
#define SC_ICON_FILENAME_DISCONNECTED "disconnected.png"
SDL_Surface *
sc_icon_load(const char *filename);
scrcpy_icon_load(void);
void
sc_icon_destroy(SDL_Surface *icon);
scrcpy_icon_destroy(SDL_Surface *icon);
#endif

View File

@@ -7,7 +7,6 @@
#include "android/input.h"
#include "android/keycodes.h"
#include "events.h"
#include "input_events.h"
#include "screen.h"
#include "shortcut_mod.h"
@@ -17,6 +16,8 @@
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
// A key/mouse processor may not be present if there is no controller
assert((!params->kp && !params->mp && !params->gp) || params->controller);
// A processor must have ops initialized
assert(!params->kp || params->kp->ops);
assert(!params->mp || params->mp->ops);
@@ -47,8 +48,6 @@ sc_input_manager_init(struct sc_input_manager *im,
im->key_repeat = 0;
im->next_sequence = 1; // 0 is reserved for SC_SEQUENCE_INVALID
im->disconnected = false;
}
static void
@@ -352,7 +351,7 @@ apply_orientation_transform(struct sc_input_manager *im,
static void
sc_input_manager_process_text_input(struct sc_input_manager *im,
const SDL_TextInputEvent *event) {
if (im->camera || !im->kp || im->screen->paused || im->disconnected) {
if (im->camera || !im->kp || im->screen->paused) {
return;
}
@@ -420,7 +419,6 @@ sc_input_manager_process_key(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
bool video = im->screen->video;
bool disconnected = im->disconnected;
SDL_Keycode sdl_keycode = event->key;
uint16_t mod = event->mod;
@@ -437,7 +435,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod)
|| sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode);
if (down && !repeat && !disconnected) {
if (down && !repeat) {
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
++im->key_repeat;
} else {
@@ -514,12 +512,6 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
}
if (disconnected) {
// Only handle shortcuts that do not interact with the device (since
// it is disconnected)
return;
}
// Flatten conditions to avoid additional indentation levels
if (control) {
// Controls for all sources
@@ -728,7 +720,7 @@ sc_input_manager_get_position(struct sc_input_manager *im, int32_t x,
static void
sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
const SDL_MouseMotionEvent *event) {
if (im->camera || !im->mp || im->screen->paused || im->disconnected) {
if (im->camera || !im->mp || im->screen->paused) {
return;
}
@@ -767,7 +759,7 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
static void
sc_input_manager_process_touch(struct sc_input_manager *im,
const SDL_TouchFingerEvent *event) {
if (im->camera || !im->mp || im->screen->paused || im->disconnected) {
if (im->camera || !im->mp || im->screen->paused) {
return;
}
@@ -822,7 +814,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// some mouse events do not interact with the device, so process the event
// even if control is disabled
if (im->camera || im->disconnected) {
if (im->camera) {
return;
}
@@ -900,7 +892,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_FRect *r = &im->screen->rect;
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
@@ -993,7 +985,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
static void
sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
const SDL_MouseWheelEvent *event) {
if (im->camera || !im->kp || im->screen->paused || im->disconnected) {
if (im->camera || !im->kp || im->screen->paused) {
return;
}
@@ -1023,7 +1015,7 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
const SDL_GamepadDeviceEvent *event) {
// Handle device added or removed even if paused
if (im->camera || !im->gp || im->disconnected) {
if (im->camera || !im->gp) {
return;
}
@@ -1068,7 +1060,7 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
static void
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
const SDL_GamepadAxisEvent *event) {
if (im->camera || !im->gp || im->screen->paused || im->disconnected) {
if (im->camera || !im->gp || im->screen->paused) {
return;
}
@@ -1088,7 +1080,7 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
static void
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
const SDL_GamepadButtonEvent *event) {
if (im->camera || !im->gp || im->screen->paused || im->disconnected) {
if (im->camera || !im->gp || im->screen->paused) {
return;
}
@@ -1114,7 +1106,7 @@ is_apk(const char *file) {
static void
sc_input_manager_process_file(struct sc_input_manager *im,
const SDL_DropEvent *event) {
if (im->camera || !im->controller || im->disconnected) {
if (im->camera || !im->controller) {
return;
}
@@ -1137,16 +1129,6 @@ sc_input_manager_process_file(struct sc_input_manager *im,
}
}
static void
sc_input_manager_on_device_disconnected(struct sc_input_manager *im) {
im->disconnected = true;
struct sc_fps_counter *fps_counter = &im->screen->fps_counter;
if (sc_fps_counter_is_started(fps_counter)) {
sc_fps_counter_stop(fps_counter);
}
}
void
sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event) {
@@ -1184,11 +1166,8 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
case SDL_EVENT_GAMEPAD_BUTTON_UP:
sc_input_manager_process_gamepad_button(im, &event->gbutton);
break;
case SDL_EVENT_DROP_FILE:
case SDL_EVENT_DROP_FILE: {
sc_input_manager_process_file(im, &event->drop);
break;
case SC_EVENT_DEVICE_DISCONNECTED:
sc_input_manager_on_device_disconnected(im);
break;
}
}
}

View File

@@ -46,8 +46,6 @@ struct sc_input_manager {
uint16_t last_mod;
uint64_t next_sequence; // used for request acknowledgements
bool disconnected;
};
struct sc_input_manager_params {

View File

@@ -39,7 +39,7 @@ main_scrcpy(int argc, char *argv[]) {
.opts = scrcpy_options_default,
.help = false,
.version = false,
.pause_on_exit = SC_PAUSE_ON_EXIT_UNDEFINED,
.pause_on_exit = SC_PAUSE_ON_EXIT_FALSE,
};
#ifndef NDEBUG

View File

@@ -444,7 +444,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
error = true;
error = false;
}
end:

View File

@@ -173,9 +173,6 @@ event_loop(struct scrcpy *s, bool has_screen) {
switch (event.type) {
case SC_EVENT_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
if (has_screen) {
sc_screen_handle_event(&s->screen, &event);
}
return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_DEMUXER_ERROR:
LOGE("Demuxer error");
@@ -202,8 +199,8 @@ event_loop(struct scrcpy *s, bool has_screen) {
break;
}
default:
if (has_screen) {
sc_screen_handle_event(&s->screen, &event);
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
}
break;
}
@@ -212,18 +209,17 @@ event_loop(struct scrcpy *s, bool has_screen) {
}
static void
terminate_runnables_on_event_loop(void) {
terminate_event_loop(void) {
sc_reject_new_runnables();
SDL_Event event;
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT,
SC_EVENT_RUN_ON_MAIN_THREAD,
SC_EVENT_RUN_ON_MAIN_THREAD) == 1) {
assert(event.type == SC_EVENT_RUN_ON_MAIN_THREAD);
// Make sure all posted runnables are run, to avoid memory leaks
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
while (SDL_PollEvent(&event)) {
if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Make sure all posted runnables are run, to avoid memory leaks
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
}
}
}
@@ -418,7 +414,6 @@ scrcpy(struct scrcpy_options *options) {
bool screen_initialized = false;
bool timeout_initialized = false;
bool timeout_started = false;
bool disconnected = false;
struct sc_acksync *acksync = NULL;
@@ -569,7 +564,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
if (options->window && options->control) {
if (options->video_playback && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
@@ -951,8 +946,15 @@ aoa_complete:
}
ret = event_loop(s, options->window);
terminate_runnables_on_event_loop();
disconnected = ret == SCRCPY_EXIT_DISCONNECTED;
terminate_event_loop();
LOGD("quit...");
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) {
@@ -997,17 +999,6 @@ end:
sc_server_stop(&s->server);
}
if (screen_initialized) {
if (disconnected) {
sc_screen_handle_disconnection(&s->screen);
}
LOGD("Quit...");
// Close the window immediately, because sc_screen_destroy() may only be
// called once the video demuxer thread is joined (it may take time)
sc_screen_hide_window(&s->screen);
}
if (timeout_started) {
sc_timeout_join(&s->timeout);
}

View File

@@ -144,121 +144,64 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
}
static void
compute_content_rect(struct sc_size render_size, struct sc_size content_size,
bool can_upscale, SDL_FRect *rect) {
if (is_optimal_size(render_size, content_size)) {
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
SDL_Rect *rect = &screen->rect;
if (is_optimal_size(drawable_size, content_size)) {
rect->x = 0;
rect->y = 0;
rect->w = render_size.width;
rect->h = render_size.height;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
return;
}
if (!can_upscale && content_size.width <= render_size.width
&& content_size.height <= render_size.height) {
// Center without upscaling
rect->x = (render_size.width - content_size.width) / 2.f;
rect->y = (render_size.height - content_size.height) / 2.f;
rect->w = content_size.width;
rect->h = content_size.height;
return;
}
bool keep_width = content_size.width * render_size.height
> content_size.height * render_size.width;
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
if (keep_width) {
rect->x = 0;
rect->w = render_size.width;
rect->h = (float) render_size.width * content_size.height
/ content_size.width;
rect->y = (render_size.height - rect->h) / 2.f;
rect->w = drawable_size.width;
rect->h = drawable_size.width * content_size.height
/ content_size.width;
rect->y = (drawable_size.height - rect->h) / 2;
} else {
rect->y = 0;
rect->h = render_size.height;
rect->w = (float) render_size.height * content_size.width
/ content_size.height;
rect->x = (render_size.width - rect->w) / 2.f;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
}
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
// Only upscale video frames, not icon
bool can_upscale = screen->video && !screen->disconnected;
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
compute_content_rect(render_size, screen->content_size, can_upscale,
&screen->rect);
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->window_shown);
assert(screen->video);
assert(screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
SDL_Renderer *renderer = screen->renderer;
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0);
sc_sdl_render_clear(renderer);
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation);
(void) res; // any error already logged
}
bool ok = false;
SDL_Texture *texture = screen->tex.texture;
if (!texture) {
// Draw a dark 10x10 square in the top-right corner to distinguish a
// black frame from the absence of a frame
struct sc_size render_size = sc_sdl_get_render_output_size(renderer);
SDL_SetRenderDrawColor(renderer, 0, 0, 0x33, 0xff);
SDL_FRect rect = {
.x = render_size.width - 20,
.y = 10,
.w = 10,
.h = 10,
};
SDL_RenderFillRect(renderer, &rect);
goto end;
}
SDL_FRect *geometry = &screen->rect;
enum sc_orientation orientation = screen->orientation;
if (orientation == SC_ORIENTATION_0) {
ok = SDL_RenderTexture(renderer, texture, NULL, geometry);
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
const SDL_FRect *dstrect = NULL;
SDL_FRect rect;
if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
rect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
dstrect = geometry;
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
ok = SDL_RenderTextureRotated(renderer, texture, NULL, dstrect, angle,
NULL, flip);
}
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
}
end:
sc_sdl_render_present(renderer);
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(_WIN32)
@@ -291,8 +234,11 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
if (ctx->width <= 0 || ctx->width > 0xFFFF
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
@@ -300,19 +246,6 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
return false;
}
// content_size can be written from this thread, because it is never read
// from the main thread before handling SC_EVENT_OPEN_WINDOW (which acts as
// a synchronization point) when video is enabled
screen->frame_size.width = session->video.width;
screen->frame_size.height = session->video.height;
screen->content_size = get_oriented_size(screen->frame_size,
screen->orientation);
bool ok = sc_push_event(SC_EVENT_OPEN_WINDOW);
if (!ok) {
return false;
}
#ifndef NDEBUG
screen->open = true;
#endif
@@ -362,12 +295,11 @@ bool
sc_screen_init(struct sc_screen *screen,
const struct sc_screen_params *params) {
screen->resize_pending = false;
screen->window_shown = false;
screen->has_frame = false;
screen->has_video_window = false;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
screen->disconnected = false;
screen->disconnect_started = false;
screen->video = params->video;
screen->camera = params->camera;
@@ -437,79 +369,41 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
screen->gl_context = NULL;
// starts with "opengl"
const char *renderer_name = SDL_GetRendererName(screen->renderer);
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
screen->gl_context = SDL_GL_CreateContext(screen->window);
if (!screen->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
goto error_destroy_renderer;
}
}
#endif
bool mipmaps = params->video;
ok = sc_texture_init(&screen->tex, screen->renderer, mipmaps);
if (!ok) {
goto error_destroy_renderer;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
goto error_destroy_texture;
goto error_destroy_window;
}
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
if (!params->video) {
screen->content_size.width = icon->w;
screen->content_size.height = icon->h;
ok = sc_texture_set_from_surface(&screen->tex, icon);
if (!ok) {
LOGE("Could not set icon: %s", SDL_GetError());
}
}
sc_icon_destroy(icon);
} else if (params->video) {
// just a warning
LOGW("Could not load icon");
} else {
// not fatal
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_window;
}
if (!params->video) {
// Make sure the content size is initialized
screen->content_size.width = 256;
screen->content_size.height = 256;
}
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();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_texture;
goto error_destroy_display;
}
struct sc_input_manager_params im_params = {
@@ -555,7 +449,6 @@ sc_screen_init(struct sc_screen *screen,
if (!screen->video) {
// Show the window immediately
screen->window_shown = true;
sc_sdl_show_window(screen->window);
if (sc_screen_is_relative_mode(screen)) {
@@ -566,15 +459,8 @@ sc_screen_init(struct sc_screen *screen,
return true;
error_destroy_texture:
sc_texture_destroy(&screen->tex);
error_destroy_renderer:
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
if (screen->gl_context) {
SDL_GL_DestroyContext(screen->gl_context);
}
#endif
SDL_DestroyRenderer(screen->renderer);
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -612,7 +498,6 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
sc_fps_counter_start(&screen->fps_counter);
}
screen->window_shown = true;
sc_sdl_show_window(screen->window);
sc_screen_update_content_rect(screen);
}
@@ -620,7 +505,6 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
void
sc_screen_hide_window(struct sc_screen *screen) {
sc_sdl_hide_window(screen->window);
screen->window_shown = false;
}
void
@@ -628,19 +512,9 @@ sc_screen_interrupt(struct sc_screen *screen) {
sc_fps_counter_interrupt(&screen->fps_counter);
}
static void
sc_screen_interrupt_disconnect(struct sc_screen *screen) {
if (screen->disconnect_started) {
sc_disconnect_interrupt(&screen->disconnect);
}
}
void
sc_screen_join(struct sc_screen *screen) {
sc_fps_counter_join(&screen->fps_counter);
if (screen->disconnect_started) {
sc_disconnect_join(&screen->disconnect);
}
}
void
@@ -648,29 +522,11 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
if (screen->disconnect_started) {
sc_disconnect_destroy(&screen->disconnect);
}
sc_texture_destroy(&screen->tex);
sc_display_destroy(&screen->display);
av_frame_free(&screen->frame);
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(screen->gl_context);
#endif
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
SDL_Event event;
int nevents = SDL_PeepEvents(&event, 1, SDL_GETEVENT,
SC_EVENT_DISCONNECTED_ICON_LOADED,
SC_EVENT_DISCONNECTED_ICON_LOADED);
if (nevents == 1) {
assert(event.type == SC_EVENT_DISCONNECTED_ICON_LOADED);
// The event was posted, but not handled, the icon must be freed
SDL_Surface *dangling_icon = event.user.data1;
sc_icon_destroy(dangling_icon);
}
}
static void
@@ -741,29 +597,62 @@ sc_screen_set_orientation(struct sc_screen *screen,
static bool
sc_screen_apply_frame(struct sc_screen *screen) {
assert(screen->video);
assert(screen->window_shown);
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
AVFrame *frame = screen->frame;
struct sc_size new_frame_size = {frame->width, frame->height};
if (screen->frame_size.width != new_frame_size.width
if (!screen->has_frame
|| screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation);
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
if (screen->has_frame) {
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
} else {
// This is the first frame
screen->has_frame = true;
screen->content_size = new_content_size;
}
enum sc_display_result res =
sc_display_prepare_texture(&screen->display, screen->frame_size,
frame->colorspace, frame->color_range);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
}
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
assert(screen->has_frame);
if (!screen->has_video_window) {
screen->has_video_window = true;
// this is the very first frame, show the window
sc_screen_show_initial_window(screen);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
}
sc_screen_render(screen, false);
return true;
@@ -807,10 +696,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
bool ok = sc_screen_apply_frame(screen);
if (!ok) {
LOGE("Resume frame update failed");
}
sc_screen_apply_frame(screen);
}
if (!paused) {
@@ -883,157 +769,59 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
content_size.height);
}
static void
sc_disconnect_on_icon_loaded(struct sc_disconnect *d, SDL_Surface *icon,
void *userdata) {
(void) d;
(void) userdata;
bool ok = sc_push_event_with_data(SC_EVENT_DISCONNECTED_ICON_LOADED, icon);
if (!ok) {
sc_icon_destroy(icon);
}
}
static void
sc_disconnect_on_timeout(struct sc_disconnect *d, void *userdata) {
(void) d;
(void) userdata;
bool ok = sc_push_event(SC_EVENT_DISCONNECTED_TIMEOUT);
(void) ok; // ignore failure
}
void
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
// !video implies !has_video_window
assert(screen->video || !screen->has_video_window);
switch (event->type) {
case SC_EVENT_OPEN_WINDOW:
sc_screen_show_initial_window(screen);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
sc_screen_render(screen, false);
return;
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
return false;
}
return;
return true;
}
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_render(screen, true);
return;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
// This event can be triggered before the window is shown
if (screen->window_shown) {
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return;
return true;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return true;
case SDL_EVENT_WINDOW_RESTORED:
if (screen->video && is_windowed(screen)) {
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
return true;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->video);
return;
assert(screen->has_video_window);
return true;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->video);
assert(screen->has_video_window);
if (is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
case SC_EVENT_DEVICE_DISCONNECTED:
assert(!screen->disconnected);
screen->disconnected = true;
if (!screen->window_shown) {
// No window open
return;
}
sc_input_manager_handle_event(&screen->im, event);
sc_texture_reset(&screen->tex);
sc_screen_render(screen, true);
sc_tick deadline = sc_tick_now() + SC_TICK_FROM_SEC(2);
static const struct sc_disconnect_callbacks cbs = {
.on_icon_loaded = sc_disconnect_on_icon_loaded,
.on_timeout = sc_disconnect_on_timeout,
};
bool ok =
sc_disconnect_start(&screen->disconnect, deadline, &cbs, NULL);
if (ok) {
screen->disconnect_started = true;
}
return;
return true;
}
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
return true;
}
sc_input_manager_handle_event(&screen->im, event);
}
void
sc_screen_handle_disconnection(struct sc_screen *screen) {
if (!screen->window_shown) {
// No window open, quit immediately
return;
}
if (!screen->disconnect_started) {
// If sc_disconnect_start() failed, quit immediately
return;
}
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_render(screen, true);
break;
case SC_EVENT_DISCONNECTED_ICON_LOADED: {
SDL_Surface *icon_disconnected = event.user.data1;
assert(icon_disconnected);
bool ok = sc_texture_set_from_surface(&screen->tex,
icon_disconnected);
if (ok) {
screen->content_size.width = icon_disconnected->w;
screen->content_size.height = icon_disconnected->h;
sc_screen_render(screen, true);
} else {
// not fatal
LOGE("Could not set disconnected icon");
}
sc_icon_destroy(icon_disconnected);
break;
}
case SC_EVENT_DISCONNECTED_TIMEOUT:
LOGD("Closing after device disconnection");
return;
case SDL_EVENT_QUIT:
LOGD("User requested to quit");
sc_screen_interrupt_disconnect(screen);
return;
default:
sc_input_manager_handle_event(&screen->im, &event);
}
}
return true;
}
struct sc_point

View File

@@ -12,21 +12,16 @@
#include "controller.h"
#include "coords.h"
#include "disconnect.h"
#include "display.h"
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
#include "mouse_capture.h"
#include "options.h"
#include "texture.h"
#include "trait/key_processor.h"
#include "trait/frame_sink.h"
#include "trait/mouse_processor.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_screen {
struct sc_frame_sink frame_sink; // frame sink trait
@@ -37,7 +32,7 @@ struct sc_screen {
bool video;
bool camera;
struct sc_texture tex;
struct sc_display display;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
@@ -54,11 +49,6 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@@ -70,17 +60,14 @@ struct sc_screen {
// client orientation
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_FRect rect;
bool window_shown;
struct SDL_Rect rect;
bool has_frame;
bool has_video_window;
AVFrame *frame;
bool paused;
AVFrame *resume_frame;
bool disconnected;
bool disconnect_started;
struct sc_disconnect disconnect;
};
struct sc_screen_params {
@@ -120,7 +107,7 @@ bool
sc_screen_init(struct sc_screen *screen, const struct sc_screen_params *params);
// request to interrupt any inner thread
// must be called before sc_screen_join()
// must be called before screen_join()
void
sc_screen_interrupt(struct sc_screen *screen);
@@ -161,13 +148,10 @@ void
sc_screen_set_paused(struct sc_screen *screen, bool paused);
// react to SDL events
void
// If this function returns false, scrcpy must exit with an error.
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
// run the event loop once the device is disconnected
void
sc_screen_handle_disconnection(struct sc_screen *screen);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels
struct sc_point

View File

@@ -3,14 +3,26 @@
#include <processthreadsapi.h>
#include <assert.h>
#include <stdlib.h>
#include "util/command.h"
#include "util/log.h"
#include "util/str.h"
#define CMD_MAX_LEN 8192
static bool
build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
// only make it work for this very specific program
// (don't handle escaping nor quotes)
size_t ret = sc_str_join(cmd, argv, ' ', len);
if (ret >= len) {
LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1);
return false;
}
return true;
}
enum sc_process_result
sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
@@ -125,9 +137,8 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
si.lpAttributeList = lpAttributeList;
}
assert(argv && *argv);
char *cmd = sc_command_serialize_windows(argv);
if (!cmd) {
char *cmd = malloc(CMD_MAX_LEN);
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
LOG_OOM();
goto error_free_attribute_list;
}

View File

@@ -1,235 +0,0 @@
#include "texture.h"
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <libavutil/pixfmt.h>
#include "util/log.h"
bool
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps) {
const char *renderer_name = SDL_GetRendererName(renderer);
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
tex->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
struct sc_opengl *gl = &tex->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
tex->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
tex->renderer = renderer;
tex->texture = NULL;
return true;
}
void
sc_texture_destroy(struct sc_texture *tex) {
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
}
static enum SDL_Colorspace
sc_texture_to_sdl_color_space(enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool full_range = color_range == AVCOL_RANGE_JPEG;
switch (color_space) {
case AVCOL_SPC_BT709:
case AVCOL_SPC_RGB:
return full_range ? SDL_COLORSPACE_BT709_FULL
: SDL_COLORSPACE_BT709_LIMITED;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
return full_range ? SDL_COLORSPACE_BT601_FULL
: SDL_COLORSPACE_BT601_LIMITED;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
return full_range ? SDL_COLORSPACE_BT2020_FULL
: SDL_COLORSPACE_BT2020_LIMITED;
default:
return SDL_COLORSPACE_JPEG;
}
}
static SDL_Texture *
sc_texture_create_frame_texture(struct sc_texture *tex,
struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
enum SDL_Colorspace sdl_color_space =
sc_texture_to_sdl_color_space(color_space, color_range);
bool ok =
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_YV12);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
SDL_TEXTUREACCESS_STREAMING);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
size.width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
size.height);
ok &= SDL_SetNumberProperty(props,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
sdl_color_space);
if (!ok) {
LOGE("Could not set texture properties");
SDL_DestroyProperties(props);
return NULL;
}
SDL_Renderer *renderer = tex->renderer;
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
SDL_DestroyProperties(props);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (tex->mipmaps) {
struct sc_opengl *gl = &tex->gl;
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
if (!props) {
LOGE("Could not get texture properties: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
const char *renderer_name = SDL_GetRendererName(tex->renderer);
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
SDL_DestroyProperties(props);
if (!texture_id) {
LOGE("Could not get texture id: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
tex->texture_id = texture_id;
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return texture;
}
bool
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame) {
struct sc_size size = {frame->width, frame->height};
assert(size.width && size.height);
if (!tex->texture
|| tex->texture_type != SC_TEXTURE_TYPE_FRAME
|| tex->texture_size.width != size.width
|| tex->texture_size.height != size.height) {
// Incompatible texture, recreate it
enum AVColorSpace color_space = frame->colorspace;
enum AVColorRange color_range = frame->color_range;
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
tex->texture = sc_texture_create_frame_texture(tex, size, color_space,
color_range);
if (!tex->texture) {
return false;
}
tex->texture_size = size;
tex->texture_type = SC_TEXTURE_TYPE_FRAME;
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
}
assert(tex->texture);
assert(tex->texture_type == SC_TEXTURE_TYPE_FRAME);
bool ok = SDL_UpdateYUVTexture(tex->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (!ok) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (tex->mipmaps) {
assert(tex->texture_id);
struct sc_opengl *gl = &tex->gl;
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
gl->GenerateMipmap(GL_TEXTURE_2D);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return true;
}
bool
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface) {
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
tex->texture = SDL_CreateTextureFromSurface(tex->renderer, surface);
if (!tex->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
tex->texture_size.width = surface->w;
tex->texture_size.height = surface->h;
tex->texture_type = SC_TEXTURE_TYPE_ICON;
return true;
}
void
sc_texture_reset(struct sc_texture *tex) {
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
tex->texture = NULL;
}
}

View File

@@ -1,47 +0,0 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavutil/frame.h>
#include <SDL3/SDL.h>
#include "coords.h"
#include "opengl.h"
enum sc_texture_type {
SC_TEXTURE_TYPE_FRAME,
SC_TEXTURE_TYPE_ICON,
};
struct sc_texture {
SDL_Renderer *renderer; // owned by the caller
SDL_Texture *texture;
// Only valid if texture != NULL
struct sc_size texture_size;
enum sc_texture_type texture_type;
struct sc_opengl gl;
bool mipmaps;
uint32_t texture_id; // only set if mipmaps is enabled
};
bool
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps);
void
sc_texture_destroy(struct sc_texture *tex);
bool
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame);
bool
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface);
void
sc_texture_reset(struct sc_texture *tex);
#endif

View File

@@ -67,8 +67,7 @@ sc_packet_source_sinks_push_session(struct sc_packet_source *source,
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_packet_sink *sink = source->sinks[i];
if (sink->ops->push_session
&& !sink->ops->push_session(sink, session)) {
if (!sink->ops->push_session(sink, session)) {
return false;
}
}

View File

@@ -9,7 +9,7 @@
# include "adb/adb.h"
#endif
#include "events.h"
#include "screen.h"
#include "usb/screen_otg.h"
#include "usb/aoa_hid.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
@@ -23,7 +23,7 @@ struct scrcpy_otg {
struct sc_mouse_aoa mouse;
struct sc_gamepad_aoa gamepad;
struct sc_screen screen;
struct sc_screen_otg screen_otg;
};
static void
@@ -31,7 +31,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
(void) usb;
(void) userdata;
sc_push_event(SC_EVENT_DEVICE_DISCONNECTED);
sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED);
}
static enum scrcpy_exit_code
@@ -39,9 +39,8 @@ event_loop(struct scrcpy_otg *s) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SC_EVENT_DEVICE_DISCONNECTED:
case SC_EVENT_USB_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
sc_screen_handle_event(&s->screen, &event);
return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_AOA_OPEN_ERROR:
LOGE("AOA open error");
@@ -50,7 +49,7 @@ event_loop(struct scrcpy_otg *s) {
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
default:
sc_screen_handle_event(&s->screen, &event);
sc_screen_otg_handle_event(&s->screen_otg, &event);
break;
}
}
@@ -89,15 +88,13 @@ scrcpy_otg(struct scrcpy_options *options) {
enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE;
struct sc_key_processor *kp = NULL;
struct sc_mouse_processor *mp = NULL;
struct sc_gamepad_processor *gp = NULL;
struct sc_keyboard_aoa *keyboard = NULL;
struct sc_mouse_aoa *mouse = NULL;
struct sc_gamepad_aoa *gamepad = NULL;
bool usb_device_initialized = false;
bool usb_connected = false;
bool aoa_started = false;
bool aoa_initialized = false;
bool screen_initialized = false;
bool disconnected = false;
#ifdef _WIN32
// On Windows, only one process could open a USB device
@@ -160,7 +157,7 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
kp = &s->keyboard.key_processor;
keyboard = &s->keyboard;
}
if (enable_mouse) {
@@ -168,12 +165,12 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
mp = &s->mouse.mouse_processor;
mouse = &s->mouse;
}
if (enable_gamepad) {
sc_gamepad_aoa_init(&s->gamepad, &s->aoa);
gp = &s->gamepad.gamepad_processor;
gamepad = &s->gamepad;
}
ok = sc_aoa_start(&s->aoa);
@@ -187,18 +184,10 @@ scrcpy_otg(struct scrcpy_options *options) {
window_title = usb_device.product ? usb_device.product : "scrcpy";
}
struct sc_screen_params params = {
.video = false,
.camera = false,
.controller = false,
.fp = NULL,
.kp = kp,
.mp = mp,
.gp = gp,
.mouse_bindings = options->mouse_bindings,
.legacy_paste = false,
.clipboard_autosync = false,
.shortcut_mods = options->shortcut_mods,
struct sc_screen_otg_params params = {
.keyboard = keyboard,
.mouse = mouse,
.gamepad = gamepad,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@@ -206,24 +195,20 @@ scrcpy_otg(struct scrcpy_options *options) {
.window_width = options->window_width,
.window_height = options->window_height,
.window_borderless = options->window_borderless,
.orientation = SC_ORIENTATION_0,
.mipmaps = options->mipmaps,
.fullscreen = false,
.start_fps_counter = false,
.shortcut_mods = options->shortcut_mods,
};
ok = sc_screen_init(&s->screen, &params);
ok = sc_screen_otg_init(&s->screen_otg, &params);
if (!ok) {
goto end;
}
screen_initialized = true;
// usb_device not needed anymore
sc_usb_device_destroy(&usb_device);
usb_device_initialized = false;
ret = event_loop(s);
disconnected = ret == SCRCPY_EXIT_DISCONNECTED;
LOGD("quit...");
end:
if (aoa_started) {
@@ -231,25 +216,13 @@ end:
}
sc_usb_stop(&s->usb);
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
if (disconnected) {
sc_screen_handle_disconnection(&s->screen);
}
LOGD("Quit...");
// Close the window immediately
sc_screen_hide_window(&s->screen);
}
if (mp) {
if (mouse) {
sc_mouse_aoa_destroy(&s->mouse);
}
if (kp) {
if (keyboard) {
sc_keyboard_aoa_destroy(&s->keyboard);
}
if (gp) {
if (gamepad) {
sc_gamepad_aoa_destroy(&s->gamepad);
}
@@ -270,10 +243,5 @@ end:
sc_usb_destroy(&s->usb);
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
}
return ret;
}

327
app/src/usb/screen_otg.c Normal file
View File

@@ -0,0 +1,327 @@
#include "screen_otg.h"
#include <assert.h>
#include <stddef.h>
#include "icon.h"
#include "options.h"
#include "util/acksync.h"
#include "util/log.h"
#include "util/sdl.h"
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
sc_sdl_render_clear(screen->renderer);
if (screen->texture) {
bool ok =
SDL_RenderTexture(screen->renderer, screen->texture, NULL, NULL);
if (!ok) {
LOGW("Could not render texture: %s", SDL_GetError());
}
}
sc_sdl_render_present(screen->renderer);
}
bool
sc_screen_otg_init(struct sc_screen_otg *screen,
const struct sc_screen_otg_params *params) {
screen->keyboard = params->keyboard;
screen->mouse = params->mouse;
screen->gamepad = params->gamepad;
const char *title = params->window_title;
assert(title);
int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED
? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED
? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED;
int width = params->window_width ? params->window_width : 256;
int height = params->window_height ? params->window_height : 256;
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
if (params->window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
return false;
}
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
bool ok = SDL_SetWindowIcon(screen->window, icon);
if (!ok) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
ok = SDL_SetRenderLogicalPresentation(screen->renderer, icon->w,
icon->h,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (!ok) {
LOGW("Could not set renderer logical size: %s", SDL_GetError());
// don't fail
}
screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon);
scrcpy_icon_destroy(icon);
if (!screen->texture) {
goto error_destroy_renderer;
}
} else {
screen->texture = NULL;
LOGW("Could not load icon");
}
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
if (screen->mouse) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
return true;
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
return false;
}
void
sc_screen_otg_destroy(struct sc_screen_otg *screen) {
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
}
static void
sc_screen_otg_process_key(struct sc_screen_otg *screen,
const SDL_KeyboardEvent *event) {
assert(screen->keyboard);
struct sc_key_processor *kp = &screen->keyboard->key_processor;
struct sc_key_event evt = {
.action = sc_action_from_sdl_keyboard_type(event->type),
.keycode = sc_keycode_from_sdl(event->key),
.scancode = sc_scancode_from_sdl(event->scancode),
.repeat = event->repeat,
.mods_state = sc_mods_state_from_sdl(event->mod),
};
assert(kp->ops->process_key);
kp->ops->process_key(kp, &evt, SC_SEQUENCE_INVALID);
}
static void
sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen,
const SDL_MouseMotionEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
struct sc_mouse_motion_event evt = {
// .position not used for HID events
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state),
};
assert(mp->ops->process_mouse_motion);
mp->ops->process_mouse_motion(mp, &evt);
}
static void
sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen,
const SDL_MouseButtonEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
struct sc_mouse_click_event evt = {
// .position not used for HID events
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_click);
mp->ops->process_mouse_click(mp, &evt);
}
static void
sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
const SDL_MouseWheelEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
struct sc_mouse_scroll_event evt = {
// .position not used for HID events
.hscroll = event->x,
.vscroll = event->y,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_scroll);
mp->ops->process_mouse_scroll(mp, &evt);
}
static void
sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
const SDL_GamepadDeviceEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
return;
}
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (!joystick) {
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
return;
}
struct sc_gamepad_device_event evt = {
.gamepad_id = SDL_GetJoystickID(joystick),
};
gp->ops->process_gamepad_added(gp, &evt);
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
} else {
LOGW("Unknown gamepad device removed");
}
struct sc_gamepad_device_event evt = {
.gamepad_id = id,
};
gp->ops->process_gamepad_removed(gp, &evt);
}
}
static void
sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
const SDL_GamepadAxisEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
}
struct sc_gamepad_axis_event evt = {
.gamepad_id = event->which,
.axis = axis,
.value = event->value,
};
gp->ops->process_gamepad_axis(gp, &evt);
}
static void
sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
const SDL_GamepadButtonEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
}
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.button = button,
};
gp->ops->process_gamepad_button(gp, &evt);
}
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
if (sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
}
switch (event->type) {
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_otg_render(screen);
break;
case SDL_EVENT_KEY_DOWN:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_EVENT_KEY_UP:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_EVENT_MOUSE_MOTION:
if (screen->mouse) {
sc_screen_otg_process_mouse_motion(screen, &event->motion);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_EVENT_MOUSE_WHEEL:
if (screen->mouse) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
// Handle device added or removed even if paused
if (screen->gamepad) {
sc_screen_otg_process_gamepad_device(screen, &event->gdevice);
}
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_axis(screen, &event->gaxis);
}
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_button(screen, &event->gbutton);
}
break;
}
}

52
app/src/usb/screen_otg.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef SC_SCREEN_OTG_H
#define SC_SCREEN_OTG_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL.h>
#include "mouse_capture.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
#include "usb/mouse_aoa.h"
struct sc_screen_otg {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_mouse_capture mc;
};
struct sc_screen_otg_params {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
const char *window_title;
bool always_on_top;
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED
uint16_t window_width;
uint16_t window_height;
bool window_borderless;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
bool
sc_screen_otg_init(struct sc_screen_otg *screen,
const struct sc_screen_otg_params *params);
void
sc_screen_otg_destroy(struct sc_screen_otg *screen);
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event);
#endif

View File

@@ -1,98 +0,0 @@
#include "command.h"
#include <stdlib.h>
#include "util/strbuf.h"
char *
sc_command_serialize_windows(const char *const argv[]) {
// <https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments>
// <https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw>
struct sc_strbuf buf;
bool ok = sc_strbuf_init(&buf, 1024);
if (!ok) {
return NULL;
}
#define BUF_PUSH(C) \
do { \
if (!sc_strbuf_append_char(&buf, C)) { \
goto end; \
} \
} while (0)
while (*argv) {
const char *arg = *argv;
BUF_PUSH('"');
// <https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments>
//
// """
// If an even number of backslashes is followed by a double quote mark,
// then one backslash (\) is placed in the argv array for every pair of
// backslashes (\\), and the double quote mark (") is interpreted as a
// string delimiter.
//
// If an odd number of backslashes is followed by a double quote mark,
// then one backslash (\) is placed in the argv array for every pair of
// backslashes (\\). The double quote mark is interpreted as an escape
// sequence by the remaining backslash, causing a literal double quote
// mark (") to be placed in argv.
// """
//
// To produce correct escaping according to what the parser will do, we
// must count the number of successive backslashes.
unsigned backslashes = 0;
for (const char *c = arg; *c; c++) {
switch (*c) {
case '"':
while (backslashes) {
// Double all backslashes before a quote
BUF_PUSH('\\');
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH('\\');
BUF_PUSH('"');
backslashes = 0;
break;
case '\\':
++backslashes;
break;
default:
while (backslashes) {
// Put all backslashes as literals
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH(*c);
break;
}
}
while (backslashes) {
// Double all backslashes before a quote
BUF_PUSH('\\');
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH('"');
++argv;
// Argument separator
if (*argv) {
BUF_PUSH(' ');
}
}
return buf.s;
end:
free(buf.s);
return NULL;
}

View File

@@ -1,17 +0,0 @@
#ifndef SC_COMMAND_H
#define SC_COMMAND_H
#include "common.h"
/**
* Serialize an argv array for Windows
*
* Convert a NULL-terminated argument array into a single escaped string
* suitable for massing to CreateProcess() on Windows.
*
* The returned value must be freed by the caller.
*/
char *
sc_command_serialize_windows(const char *const argv[]);
#endif

View File

@@ -5,25 +5,6 @@
#include "util/log.h"
char *
sc_file_build_path(const char *dir, const char *name) {
size_t dir_len = strlen(dir);
size_t name_len = strlen(name);
size_t len = dir_len + name_len + 2; // +2: '/' and '\0'
char *path = malloc(len);
if (!path) {
LOG_OOM();
return NULL;
}
memcpy(path, dir, dir_len);
path[dir_len] = SC_PATH_SEPARATOR;
// namelen + 1 to copy the final '\0'
memcpy(&path[dir_len + 1], name, name_len + 1);
return path;
}
char *
sc_file_get_local_path(const char *name) {
char *executable_path = sc_file_get_executable_path();
@@ -44,9 +25,24 @@ sc_file_get_local_path(const char *name) {
*p = '\0'; // modify executable_path in place
char *dir = executable_path;
char *file_path = sc_file_build_path(dir, name);
size_t dirlen = strlen(dir);
size_t namelen = strlen(name);
size_t len = dirlen + namelen + 2; // +2: '/' and '\0'
char *file_path = malloc(len);
if (!file_path) {
LOG_OOM();
free(executable_path);
return NULL;
}
memcpy(file_path, dir, dirlen);
file_path[dirlen] = SC_PATH_SEPARATOR;
// namelen + 1 to copy the final '\0'
memcpy(&file_path[dirlen + 1], name, namelen + 1);
free(executable_path);
return file_path;
}

View File

@@ -40,15 +40,6 @@ sc_file_get_executable_path(void);
char *
sc_file_get_local_path(const char *name);
/**
* Return the concatenation of dir, the path separator and the filename.
*
* The result must be freed by the caller using free(). It may return NULL on
* error.
*/
char *
sc_file_build_path(const char *dir, const char *filename);
/**
* Indicate if the file exists and is not a directory
*/

View File

@@ -151,10 +151,6 @@ sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
void
sc_log_configure(void) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
SDL_SetLogOutputFunction(sc_sdl_log_print, NULL);
// Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback);

View File

@@ -130,25 +130,6 @@ sc_sdl_hide_window(SDL_Window *window) {
}
}
struct sc_size
sc_sdl_get_render_output_size(SDL_Renderer *renderer) {
int width;
int height;
bool ok = SDL_GetRenderOutputSize(renderer, &width, &height);
if (!ok) {
LOGE("Could not get render output size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
bool
sc_sdl_render_clear(SDL_Renderer *renderer) {
bool ok = SDL_RenderClear(renderer);

View File

@@ -34,9 +34,6 @@ sc_sdl_show_window(SDL_Window *window);
void
sc_sdl_hide_window(SDL_Window *window);
struct sc_size
sc_sdl_get_render_output_size(SDL_Renderer *renderer);
bool
sc_sdl_render_clear(SDL_Renderer *renderer);

View File

@@ -49,6 +49,21 @@ truncated:
return n;
}
char *
sc_str_quote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
if (!quoted) {
LOG_OOM();
return NULL;
}
memcpy(&quoted[1], src, len);
quoted[0] = '"';
quoted[len + 1] = '"';
quoted[len + 2] = '\0';
return quoted;
}
char *
sc_str_concat(const char *start, const char *end) {
assert(start);

View File

@@ -32,6 +32,14 @@ sc_strncpy(char *dest, const char *src, size_t n);
size_t
sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
/**
* Quote a string
*
* Return a new allocated string, surrounded with quotes (`"`).
*/
char *
sc_str_quote(const char *src);
/**
* Concat two strings
*

View File

@@ -163,45 +163,6 @@ static void test_adb_devices_spaces(void) {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_serial_with_spaces(void) {
char output[] =
"List of devices attached\n"
"adb-0123456789AB-CDdefg (2)._adb-tls-connect._tcp device "
"product:blazer model:Pixel_10_Pro device:blazer transport_id:3\n";
struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER;
bool ok = sc_adb_parse_devices(output, &vec);
assert(ok);
assert(vec.size == 1);
struct sc_adb_device *device = &vec.data[0];
assert(!strcmp("adb-0123456789AB-CDdefg (2)._adb-tls-connect._tcp",
device->serial));
assert(!strcmp("device", device->state));
assert(!strcmp("Pixel_10_Pro", device->model));
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_with_devpath_without_colon(void) {
char output[] =
"List of devices attached\n"
"12345678 device 3-1 product:manet model:23117RK66C "
"device:manet transport_id:2\n";
struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER;
bool ok = sc_adb_parse_devices(output, &vec);
assert(ok);
assert(vec.size == 1);
struct sc_adb_device *device = &vec.data[0];
assert(!strcmp("12345678", device->serial));
assert(!strcmp("device", device->state));
assert(!strcmp("23117RK66C", device->model));
sc_adb_devices_destroy(&vec);
}
static void test_get_ip_single_line(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34\r\r\n";
@@ -304,8 +265,6 @@ int main(int argc, char *argv[]) {
test_adb_devices_without_header();
test_adb_devices_corrupted();
test_adb_devices_spaces();
test_adb_devices_serial_with_spaces();
test_adb_devices_with_devpath_without_colon();
test_get_ip_single_line();
test_get_ip_single_line_without_eol();

View File

@@ -1,61 +0,0 @@
#include "common.h"
#include <assert.h>
#include <stdlib.h>
#include "util/command.h"
static void test_command_with_spaces(void) {
const char *const argv[] = {
"C:\\Program Files\\scrcpy\\adb",
"-s",
"serial with spaces",
"push",
"E:\\some folder\\scrcpy-server",
"/data/local/tmp/scrcpy-server.jar",
NULL,
};
char *cmd = sc_command_serialize_windows(argv);
const char *expected = "\"C:\\Program Files\\scrcpy\\adb\" "
"\"-s\" "
"\"serial with spaces\" "
"\"push\" "
"\"E:\\some folder\\scrcpy-server\" "
"\"/data/local/tmp/scrcpy-server.jar\"";
assert(!strcmp(expected, cmd));
free(cmd);
}
static void test_command_with_backslashes(void) {
const char *const argv[] = {
"a\\\\ b\\",
"def \\",
"gh\"i\" \\\\",
"jkl\\\\",
"mno\\",
"p\\\"qr",
NULL,
};
char *cmd = sc_command_serialize_windows(argv);
const char *expected = "\"a\\\\ b\\\\\" "
"\"def \\\\\" "
"\"gh\\\"i\\\" \\\\\\\\\" "
"\"jkl\\\\\\\\\" "
"\"mno\\\\\" "
"\"p\\\\\\\"qr\"";
assert(!strcmp(expected, cmd));
free(cmd);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_command_with_spaces();
test_command_with_backslashes();
return 0;
}

View File

@@ -131,6 +131,16 @@ static void test_join_truncated_after_sep(void) {
assert(!strcmp("abc de ", s));
}
static void test_quote(void) {
const char *s = "abcde";
char *out = sc_str_quote(s);
// add '"' at the beginning and the end
assert(!strcmp("\"abcde\"", out));
free(out);
}
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
@@ -388,6 +398,7 @@ int main(int argc, char *argv[]) {
test_join_truncated_in_token();
test_join_truncated_before_sep();
test_join_truncated_after_sep();
test_quote();
test_concat();
test_utf8_truncate();
test_parse_integer();

View File

@@ -72,6 +72,18 @@ Documentation for command line arguments is available:
- `scrcpy --help`
- on [github](/README.md)
To start scrcpy directly without opening a terminal, double-click on one of
these files:
- `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy
terminates, unless an error occurs);
- `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error
message).
_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would
close immediately and you won't have time to read any error message (this
executable is intended to be run from the terminal). Use `scrcpy-console.bat`
instead._
If you plan to always use the same arguments, create a file `myscrcpy.bat`
(enable [show file extensions] to avoid confusion) containing your command, For
example:
@@ -80,17 +92,9 @@ example:
scrcpy --prefer-text --turn-screen-off --stay-awake
```
Add `--pause-on-exit=if-error` if you want the console to remain open when
scrcpy fails:
```bash
scrcpy --prefer-text --turn-screen-off --stay-awake --pause-on-exit=if-error
```
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
Then just double-click on that file to run it.
Then just double-click on that file.
To start scrcpy without opening a terminal, double-click `scrcpy-noconsole.vbs`
(note that errors won't be shown). To pass arguments, edit (a copy of)
`scrcpy-noconsole.vbs` and add the desired arguments.
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
to add some arguments.

View File

@@ -22,12 +22,9 @@ app/deps/libusb.sh linux native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$LINUX_BUILD_DIR"
meson setup "$LINUX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \
@@ -41,6 +38,6 @@ ninja -C "$LINUX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$LINUX_BUILD_DIR/dist"
cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$LINUX_BUILD_DIR/dist/"
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

View File

@@ -22,12 +22,9 @@ app/deps/libusb.sh macos native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$MACOS_BUILD_DIR"
meson setup "$MACOS_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \
@@ -41,6 +38,6 @@ ninja -C "$MACOS_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$MACOS_BUILD_DIR/dist"
cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$MACOS_BUILD_DIR/dist/"
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"

View File

@@ -29,12 +29,9 @@ app/deps/libusb.sh $WINXX cross shared
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$WINXX_BUILD_DIR"
meson setup "$WINXX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--cross-file=cross_$WINXX.txt \
@@ -48,8 +45,9 @@ ninja -C "$WINXX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$WINXX_BUILD_DIR/dist"
cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$WINXX_BUILD_DIR/dist/"
cp app/data/icon.png "$WINXX_BUILD_DIR/dist/"
cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/"
cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/"

2
run
View File

@@ -20,6 +20,6 @@ then
exit 1
fi
SCRCPY_ICON_DIR="app/data" \
SCRCPY_ICON_PATH="app/data/icon.png" \
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \
"$BUILDDIR/app/scrcpy" "$@"

View File

@@ -266,14 +266,14 @@ public final class AudioEncoder implements AsyncProcessor {
outputThread.start();
waitEnded();
} catch (AudioCaptureException e) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(false);
throw e;
} catch (Throwable e) {
} catch (ConfigurationException e) {
// Notify the error to make scrcpy exit
streamer.writeDisableStream(true);
throw e;
} catch (Throwable e) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(false);
throw e;
} finally {
// Cleanup everything (either at the end or on error at any step of the initialization)
if (mediaCodecThread != null) {

View File

@@ -263,7 +263,7 @@ public class CameraCapture extends SurfaceCapture {
return ratio.getAspectRatio();
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
@TargetApi(AndroidVersions.API_28_ANDROID_9)
@Override
public void start(Surface surface) throws IOException {
if (transform != null) {
@@ -474,7 +474,6 @@ public class CameraCapture extends SurfaceCapture {
});
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
private void zoom(boolean in) {
cameraHandler.post(() -> {
assertCameraThread();

View File

@@ -264,10 +264,6 @@ public class SurfaceEncoder implements AsyncProcessor {
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
// real-time priority
format.setInteger(MediaFormat.KEY_PRIORITY, 0);
// output 1 frame as soon as 1 frame is queued
format.setInteger(MediaFormat.KEY_LATENCY, 1);
if (maxFps > 0) {
// The key existed privately before Android 10:
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>