Compare commits

...

31 Commits

Author SHA1 Message Date
Romain Vimont
c2e9aa85e3 rename display to texture 2026-01-24 22:41:34 +01:00
Romain Vimont
82c0ffc69b set_texture_from_frame 2026-01-24 22:41:34 +01:00
Romain Vimont
01cef27bea set_texture_from_surface 2026-01-24 22:41:34 +01:00
Romain Vimont
2f54970f60 frect 2026-01-24 22:41:34 +01:00
Romain Vimont
665fce0707 render icon using common code can_upscale 2026-01-24 22:41:34 +01:00
Romain Vimont
cea820a057 update_rect 2026-01-24 22:41:34 +01:00
Romain Vimont
6f29e9427e use render_output_size 2026-01-24 22:41:34 +01:00
Romain Vimont
8803f452a3 render_output_size 2026-01-24 22:41:33 +01:00
Romain Vimont
0fcf332b37 render_from_screen 2026-01-24 22:40:56 +01:00
Romain Vimont
0cd5c35f19 Move renderer from sc_display to sc_screen
Make sc_screen the owner of both the SDL window and the SDL renderer.
This is the first step toward limiting the role of sc_display to texture
management.
2026-01-24 22:40:54 +01:00
Romain Vimont
63961db125 Simplify texture failure handling
When the scrcpy window is minimized on Windows with D3D9, texture
creation and updates fail.

As a workaround, a mechanism was implemented to reattempt applying the
requested changes.

Since SDL3 defaults to the D3D11 backend, remove this workaround,
which adds a lot of complexity for a backend that should almost never
be used.

However, do not close scrcpy when texture creation or updates fail; only
the frame should be lost.

Refs SDL/#7651 <https://github.com/libsdl-org/SDL/issues/7651>
Refs #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
Refs 6298ef095f
2026-01-24 22:38:06 +01:00
Romain Vimont
503ff8ac79 icon 2026-01-24 22:19:31 +01:00
Romain Vimont
089c62b1a2 fix_window_condition 2026-01-24 22:19:14 +01:00
Romain Vimont
72a2ae3224 SCRCPY_ICON_DIR 2026-01-17 22:06:09 +01:00
Romain Vimont
d9669a2610 sc_file_build_path 2026-01-17 22:00:31 +01:00
Romain Vimont
f1cb2213b2 Rename icon.png to scrcpy.png
This makes the icon name consistent everywhere.
2026-01-17 21:40:53 +01:00
Romain Vimont
373b366fa5 Do not attempt to increase audio thread priority
In practice, this always fails for non-privileged processes.
2026-01-17 21:30:19 +01:00
Romain Vimont
5607f12f2a Document camera torch and zoom features
PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:41:09 +01:00
Tommie
8ac04d39f4 Add shortcuts to change the camera zoom
MOD+up and MOD+zoom change the camera zoom.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:40:58 +01:00
Tommie
af355804ef Add option to specify the camera zoom
Add --camera-zoom to specify the camera zoom.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:37:42 +01:00
Romain Vimont
1018f3e9be 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.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:36:33 +01:00
Romain Vimont
48fcfdd104 Add shortcuts to switch the camera torch
MOD+t turns on the camera torch, MOD+Shift+t turns it off.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-13 20:35:52 +01:00
Tommie
553813e97d Add option to turn on the camera torch
Add --camera-torch to turn on the camera torch when the camera starts.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:32:06 +01:00
Romain Vimont
968f178205 Simplify camera startup code
Avoid multiple back-and-forths between the caller thread and the camera
thread.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:28:32 +01:00
Romain Vimont
1b13d0a22d Enable "reset video" shortcut for camera
Make the existing "reset video" feature (MOD+Shift+R) also work for a
camera video source.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:27:53 +01:00
Romain Vimont
078565d40b Enable controls for camera video source
This will allow the implementation of camera-specific shortcuts.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-13 20:27:33 +01:00
Romain Vimont
cc7c07d4a3 Report control protocol errors
All IOExceptions were ignored to avoid an error on close, but protocol
exceptions must be reported.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:18:36 +01:00
Romain Vimont
24b46f11a5 Throw error on unexpected control message type
PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:15:37 +01:00
Romain Vimont
f348a1f307 Group control-event shortcuts
Group together the shortcuts that trigger control events sent to the
device.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:11:16 +01:00
Romain Vimont
f8846aa76d Simplify capture invalidation
Remove the unnecessary requestInvalidate() indirection. Use a single
invalidate() method instead.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:10:30 +01:00
Romain Vimont
1e51d2c83f Move precondition checks for input events
Always call the appropriate method responsible for handling the input
event, which can then decide to do nothing.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:08:40 +01:00
46 changed files with 1335 additions and 915 deletions

View File

@@ -18,6 +18,8 @@ _scrcpy() {
--camera-fps=
--camera-high-speed
--camera-size=
--camera-torch
--camera-zoom=
--capture-orientation=
--crop=
-d --select-usb
@@ -197,6 +199,8 @@ _scrcpy() {
|--camera-id \
|--camera-fps \
|--camera-size \
|--camera-torch \
|--camera-zoom \
|--crop \
|--display-id \
|--max-fps \

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

@@ -25,6 +25,8 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]'
'--camera-torch[Turn on the camera torch when the camera starts]'
'--camera-zoom[Specify the camera zoom initial value]'
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'

View File

@@ -15,7 +15,6 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/events.c',
'src/icon.c',
'src/file_pusher.c',
@@ -33,6 +32,7 @@ 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',
@@ -191,8 +191,7 @@ executable('scrcpy', src,
datadir = get_option('datadir') # by default 'share'
install_man('scrcpy.1')
install_data('data/icon.png',
rename: 'scrcpy.png',
install_data('data/scrcpy.png',
install_dir: datadir / 'icons/hicolor/256x256/apps')
install_data('data/zsh-completion/_scrcpy',
install_dir: datadir / 'zsh/site-functions')
@@ -283,6 +282,6 @@ endif
if meson.version().version_compare('>= 0.58.0')
devenv = environment()
devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png')
devenv.set('SCRCPY_ICON_DIR', meson.current_source_dir() / 'data')
meson.add_devenv(devenv)
endif

View File

@@ -131,6 +131,14 @@ The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
.TP
.BI \-\-camera\-torch
Turn on the camera torch when the camera starts.
.TP
.BI "\-\-camera-zoom " zoom
Specify the camera zoom initial value.
.TP
.BI "\-\-capture\-orientation " value
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
@@ -815,6 +823,22 @@ Install APK from computer
.B Drag & drop non-APK file
Push file to device (see \fB\-\-push\-target\fR)
.TP
.B MOD+t
Turn on the camera torch (camera mode only)
.TP
.B MOD+Shift+t
Turn off the camera torch (camera mode only)
.TP
.B MOD+Up
Zoom camera in (camera mode only)
.TP
.B MOD+Down
Zoom camera out (camera mode only)
.SH Environment variables
@@ -827,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_PATH
Path to the program icon.
.B SCRCPY_ICON_DIR
Path to the icon directory.
.TP
.B SCRCPY_SERVER_PATH

View File

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

@@ -114,6 +114,8 @@ enum {
OPT_NO_VD_SYSTEM_DECORATIONS,
OPT_NO_VD_DESTROY_CONTENT,
OPT_DISPLAY_IME_POLICY,
OPT_CAMERA_TORCH,
OPT_CAMERA_ZOOM,
};
struct sc_option {
@@ -313,6 +315,17 @@ static const struct sc_option options[] = {
.argdesc = "<width>x<height>",
.text = "Specify an explicit camera capture size.",
},
{
.longopt_id = OPT_CAMERA_TORCH,
.longopt = "camera-torch",
.text = "Turn on the camera torch when the camera starts.",
},
{
.longopt_id = OPT_CAMERA_ZOOM,
.longopt = "camera-zoom",
.argdesc = "zoom",
.text = "Specify the camera zoom initial value.",
},
{
.longopt_id = OPT_CAPTURE_ORIENTATION,
.longopt = "capture-orientation",
@@ -1207,6 +1220,22 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "Drag & drop non-APK file" },
.text = "Push file to device (see --push-target)",
},
{
.shortcuts = { "MOD+t" },
.text = "Turn on the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Shift+t" },
.text = "Turn off the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Up" },
.text = "Zoom camera in (camera mode only)",
},
{
.shortcuts = { "MOD+Down" },
.text = "Zoom camera out (camera mode only)",
},
};
static const struct sc_envvar envvars[] = {
@@ -1220,8 +1249,8 @@ static const struct sc_envvar envvars[] = {
"--tcpip=<addr>) is specified",
},
{
.name = "SCRCPY_ICON_PATH",
.text = "Path to the program icon",
.name = "SCRCPY_ICON_DIR",
.text = "Path to the icon directory",
},
{
.name = "SCRCPY_SERVER_PATH",
@@ -2780,6 +2809,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_CAMERA_HIGH_SPEED:
opts->camera_high_speed = true;
break;
case OPT_CAMERA_TORCH:
opts->camera_torch = true;
break;
case OPT_CAMERA_ZOOM:
opts->camera_zoom = optarg;
break;
case OPT_NO_WINDOW:
opts->window = false;
break;
@@ -2928,7 +2963,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
if (opts->control) {
if (opts->control && opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_SDK;
@@ -3106,8 +3141,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->control) {
LOGI("Camera video source: control disabled");
opts->control = false;
// Disable all inputs for camera
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_DISABLED;
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
}
} else if (opts->camera_id
|| opts->camera_ar

View File

@@ -182,12 +182,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
return 1 + len;
}
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
buf[1] = msg->camera_set_torch.on ? 1 : 0;
return 2;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
// no additional data
return 1;
default:
@@ -318,6 +323,16 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
LOG_CMSG("camera set torch %s",
msg->camera_set_torch.on ? "on" : "off");
break;
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
LOG_CMSG("camera zoom in");
break;
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
LOG_CMSG("camera zoom out");
break;
default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break;

View File

@@ -43,6 +43,9 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
};
enum sc_copy_key {
@@ -111,6 +114,9 @@ struct sc_control_msg {
struct {
char *name;
} start_app;
struct {
bool on;
} camera_set_torch;
};
};

View File

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

View File

@@ -1,69 +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"
#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

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

View File

@@ -6,9 +6,12 @@
#include <SDL3/SDL_surface.h>
SDL_Surface *
scrcpy_icon_load(void);
sc_icon_load(const char *path);
SDL_Surface *
sc_icon_load_scrcpy(void);
void
scrcpy_icon_destroy(SDL_Surface *icon);
sc_icon_destroy(SDL_Surface *icon);
#endif

View File

@@ -29,6 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im,
im->kp = params->kp;
im->mp = params->mp;
im->gp = params->gp;
im->camera = params->camera;
im->mouse_bindings = params->mouse_bindings;
im->legacy_paste = params->legacy_paste;
@@ -52,7 +53,7 @@ sc_input_manager_init(struct sc_input_manager *im,
static void
send_keycode(struct sc_input_manager *im, enum android_keycode keycode,
enum sc_action action, const char *name) {
assert(im->controller && im->kp);
assert(im->controller && im->kp && !im->camera);
// send DOWN event
struct sc_control_msg msg;
@@ -109,7 +110,7 @@ action_menu(struct sc_input_manager *im, enum sc_action action) {
static void
press_back_or_turn_screen_on(struct sc_input_manager *im,
enum sc_action action) {
assert(im->controller && im->kp);
assert(im->controller && im->kp && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
@@ -124,7 +125,7 @@ press_back_or_turn_screen_on(struct sc_input_manager *im,
static void
expand_notification_panel(struct sc_input_manager *im) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
@@ -136,7 +137,7 @@ expand_notification_panel(struct sc_input_manager *im) {
static void
expand_settings_panel(struct sc_input_manager *im) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
@@ -148,7 +149,7 @@ expand_settings_panel(struct sc_input_manager *im) {
static void
collapse_panels(struct sc_input_manager *im) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS;
@@ -160,7 +161,7 @@ collapse_panels(struct sc_input_manager *im) {
static bool
get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
assert(im->controller && im->kp);
assert(im->controller && im->kp && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD;
@@ -177,7 +178,7 @@ get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
static bool
set_device_clipboard(struct sc_input_manager *im, bool paste,
uint64_t sequence) {
assert(im->controller && im->kp);
assert(im->controller && im->kp && !im->camera);
char *text = SDL_GetClipboardText();
if (!text) {
@@ -209,7 +210,7 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
static void
set_display_power(struct sc_input_manager *im, bool on) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
@@ -236,7 +237,7 @@ switch_fps_counter_state(struct sc_input_manager *im) {
static void
clipboard_paste(struct sc_input_manager *im) {
assert(im->controller && im->kp);
assert(im->controller && im->kp && !im->camera);
char *text = SDL_GetClipboardText();
if (!text) {
@@ -267,7 +268,7 @@ clipboard_paste(struct sc_input_manager *im) {
static void
rotate_device(struct sc_input_manager *im) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE;
@@ -279,7 +280,7 @@ rotate_device(struct sc_input_manager *im) {
static void
open_hard_keyboard_settings(struct sc_input_manager *im) {
assert(im->controller);
assert(im->controller && !im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS;
@@ -301,6 +302,43 @@ reset_video(struct sc_input_manager *im) {
}
}
static void
camera_set_torch(struct sc_input_manager *im, bool on) {
assert(im->controller && im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH;
msg.camera_set_torch.on = on;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request setting camera torch");
}
}
static void
camera_zoom_in(struct sc_input_manager *im) {
assert(im->controller && im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request camera zoom in");
}
}
static void
camera_zoom_out(struct sc_input_manager *im) {
assert(im->controller && im->camera);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request camera zoom out");
}
}
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@@ -313,6 +351,10 @@ 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) {
return;
}
if (!im->kp->ops->process_text) {
// The key processor does not support text input
return;
@@ -370,6 +412,9 @@ inverse_point(struct sc_point point, struct sc_size size,
static void
sc_input_manager_process_key(struct sc_input_manager *im,
const SDL_KeyboardEvent *event) {
// some key events do not interact with the device, so process the event
// even if control is disabled
// controller is NULL if --no-control is requested
bool control = im->controller;
bool paused = im->screen->paused;
@@ -403,106 +448,45 @@ sc_input_manager_process_key(struct sc_input_manager *im,
if (is_shortcut) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
switch (sdl_keycode) {
case SDLK_H:
if (im->kp && !shift && !repeat && !paused) {
action_home(im, action);
}
return;
case SDLK_B: // fall-through
case SDLK_BACKSPACE:
if (im->kp && !shift && !repeat && !paused) {
action_back(im, action);
}
return;
case SDLK_S:
if (im->kp && !shift && !repeat && !paused) {
action_app_switch(im, action);
}
return;
case SDLK_M:
if (im->kp && !shift && !repeat && !paused) {
action_menu(im, action);
}
return;
case SDLK_P:
if (im->kp && !shift && !repeat && !paused) {
action_power(im, action);
}
return;
case SDLK_O:
if (control && !repeat && down && !paused) {
bool on = shift;
set_display_power(im, on);
}
return;
case SDLK_Z:
if (video && down && !repeat) {
sc_screen_set_paused(im->screen, !shift);
}
return;
case SDLK_DOWN:
// Only capture if shift is set
if (shift) {
if (video && !repeat && down) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180);
}
} else if (im->kp && !paused) {
// forward repeated events
action_volume_down(im, action);
return;
}
return;
break;
case SDLK_UP:
// Only capture if shift is set
if (shift) {
if (video && !repeat && down) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180);
apply_orientation_transform(im, SC_ORIENTATION_FLIP_180);
}
} else if (im->kp && !paused) {
// forward repeated events
action_volume_up(im, action);
return;
}
return;
break;
case SDLK_LEFT:
if (video && !repeat && down) {
if (shift) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0);
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im,
SC_ORIENTATION_270);
apply_orientation_transform(im, SC_ORIENTATION_270);
}
}
return;
case SDLK_RIGHT:
if (video && !repeat && down) {
if (shift) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0);
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im,
SC_ORIENTATION_90);
}
}
return;
case SDLK_C:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_COPY);
}
return;
case SDLK_X:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_CUT);
}
return;
case SDLK_V:
if (im->kp && !repeat && down && !paused) {
if (shift || im->legacy_paste) {
// inject the text as input events
clipboard_paste(im);
} else {
// store the text in the device clipboard and paste,
// without requesting an acknowledgment
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
apply_orientation_transform(im, SC_ORIENTATION_90);
}
}
return;
@@ -526,33 +510,134 @@ sc_input_manager_process_key(struct sc_input_manager *im,
switch_fps_counter_state(im);
}
return;
case SDLK_N:
if (control && !repeat && down && !paused) {
if (shift) {
collapse_panels(im);
} else if (im->key_repeat == 0) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
}
}
return;
case SDLK_R:
if (control && !repeat && down && !paused) {
if (shift) {
}
// Flatten conditions to avoid additional indentation levels
if (control) {
// Controls for all sources
switch (sdl_keycode) {
case SDLK_R:
if (!repeat && shift && down && !paused) {
reset_video(im);
} else {
}
return;
}
}
if (control && !im->camera) {
switch (sdl_keycode) {
case SDLK_H:
if (im->kp && !shift && !repeat && !paused) {
action_home(im, action);
}
return;
case SDLK_B: // fall-through
case SDLK_BACKSPACE:
if (im->kp && !shift && !repeat && !paused) {
action_back(im, action);
}
return;
case SDLK_S:
if (im->kp && !shift && !repeat && !paused) {
action_app_switch(im, action);
}
return;
case SDLK_M:
if (im->kp && !shift && !repeat && !paused) {
action_menu(im, action);
}
return;
case SDLK_P:
if (im->kp && !shift && !repeat && !paused) {
action_power(im, action);
}
return;
case SDLK_O:
if (control && !repeat && down && !paused) {
bool on = shift;
set_display_power(im, on);
}
return;
case SDLK_DOWN:
if (im->kp && !shift && !paused) {
// forward repeated events
action_volume_down(im, action);
}
return;
case SDLK_UP:
if (im->kp && !shift && !paused) {
// forward repeated events
action_volume_up(im, action);
}
return;
case SDLK_C:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_COPY);
}
return;
case SDLK_X:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_CUT);
}
return;
case SDLK_V:
if (im->kp && !repeat && down && !paused) {
if (shift || im->legacy_paste) {
// inject the text as input events
clipboard_paste(im);
} else {
// store the text in the device clipboard and paste,
// without requesting an acknowledgment
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
}
}
return;
case SDLK_N:
if (!repeat && down && !paused) {
if (shift) {
collapse_panels(im);
} else if (im->key_repeat == 0) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
}
}
return;
case SDLK_R:
if (!repeat && !shift && down && !paused) {
rotate_device(im);
}
}
return;
case SDLK_K:
if (control && !shift && !repeat && down && !paused
&& im->kp && im->kp->hid) {
// Only if the current keyboard is hid
open_hard_keyboard_settings(im);
}
return;
return;
case SDLK_K:
if (!shift && !repeat && down && !paused
&& im->kp && im->kp->hid) {
// Only if the current keyboard is hid
open_hard_keyboard_settings(im);
}
return;
}
}
if (control && im->camera) {
switch (sdl_keycode) {
case SDLK_T:
if (!repeat && down) {
camera_set_torch(im, !shift);
}
return;
case SDLK_DOWN:
if (!shift && down && !paused) {
// forward repeated events
camera_zoom_out(im);
}
return;
case SDLK_UP:
if (!shift && down && !paused) {
// forward repeated events
camera_zoom_in(im);
}
return;
}
}
return;
@@ -562,6 +647,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
}
assert(!im->camera);
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_V && down && !repeat;
if (im->clipboard_autosync && is_ctrl_v) {
@@ -633,6 +720,10 @@ 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) {
return;
}
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
@@ -668,6 +759,10 @@ 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) {
return;
}
if (!im->mp->ops->process_touch) {
// The mouse processor does not support touch events
return;
@@ -716,6 +811,13 @@ sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
static void
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
const SDL_MouseButtonEvent *event) {
// some mouse events do not interact with the device, so process the event
// even if control is disabled
if (im->camera) {
return;
}
if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate
return;
@@ -790,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_Rect *r = &im->screen->rect;
SDL_FRect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
@@ -883,6 +985,10 @@ 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) {
return;
}
if (!im->mp->ops->process_mouse_scroll) {
// The mouse processor does not support scroll events
return;
@@ -907,6 +1013,12 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
static void
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) {
return;
}
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
@@ -948,6 +1060,10 @@ 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) {
return;
}
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
@@ -964,6 +1080,10 @@ 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) {
return;
}
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
@@ -986,6 +1106,10 @@ 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) {
return;
}
assert(event->type == SDL_EVENT_DROP_FILE);
char *file = strdup(event->data);
if (!file) {
@@ -1008,72 +1132,41 @@ sc_input_manager_process_file(struct sc_input_manager *im,
void
sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event) {
bool control = im->controller;
bool paused = im->screen->paused;
switch (event->type) {
case SDL_EVENT_TEXT_INPUT:
if (!im->kp || paused) {
break;
}
sc_input_manager_process_text_input(im, &event->text);
break;
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
// some key events do not interact with the device, so process the
// event even if control is disabled
sc_input_manager_process_key(im, &event->key);
break;
case SDL_EVENT_MOUSE_MOTION:
if (!im->mp || paused) {
break;
}
sc_input_manager_process_mouse_motion(im, &event->motion);
break;
case SDL_EVENT_MOUSE_WHEEL:
if (!im->mp || paused) {
break;
}
sc_input_manager_process_mouse_wheel(im, &event->wheel);
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
sc_input_manager_process_mouse_button(im, &event->button);
break;
case SDL_EVENT_FINGER_MOTION:
case SDL_EVENT_FINGER_DOWN:
case SDL_EVENT_FINGER_UP:
if (!im->mp || paused) {
break;
}
sc_input_manager_process_touch(im, &event->tfinger);
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
// Handle device added or removed even if paused
if (!im->gp) {
break;
}
sc_input_manager_process_gamepad_device(im, &event->gdevice);
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
if (!im->gp || paused) {
break;
}
sc_input_manager_process_gamepad_axis(im, &event->gaxis);
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
if (!im->gp || paused) {
break;
}
sc_input_manager_process_gamepad_button(im, &event->gbutton);
break;
case SDL_EVENT_DROP_FILE: {
if (!control) {
break;
}
sc_input_manager_process_file(im, &event->drop);
}
}

View File

@@ -24,6 +24,8 @@ struct sc_input_manager {
struct sc_mouse_processor *mp;
struct sc_gamepad_processor *gp;
bool camera;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
@@ -53,6 +55,7 @@ struct sc_input_manager_params {
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
struct sc_gamepad_processor *gp;
bool camera;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;

View File

@@ -16,6 +16,7 @@ const struct scrcpy_options scrcpy_options_default = {
.camera_id = NULL,
.camera_size = NULL,
.camera_ar = NULL,
.camera_zoom = NULL,
.camera_fps = 0,
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
@@ -113,6 +114,7 @@ const struct scrcpy_options scrcpy_options_default = {
.angle = NULL,
.vd_destroy_content = true,
.vd_system_decorations = true,
.camera_torch = false,
};
enum sc_orientation

View File

@@ -241,6 +241,7 @@ struct scrcpy_options {
const char *camera_id;
const char *camera_size;
const char *camera_ar;
const char *camera_zoom;
uint16_t camera_fps;
enum sc_log_level log_level;
enum sc_codec video_codec;
@@ -327,6 +328,7 @@ struct scrcpy_options {
const char *start_app;
bool vd_destroy_content;
bool vd_system_decorations;
bool camera_torch;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@@ -469,6 +469,8 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed,
.camera_torch = options->camera_torch,
.camera_zoom = options->camera_zoom,
.vd_destroy_content = options->vd_destroy_content,
.vd_system_decorations = options->vd_system_decorations,
.list = options->list,
@@ -802,6 +804,7 @@ aoa_complete:
struct sc_screen_params screen_params = {
.video = options->video_playback,
.camera = options->video_source == SC_VIDEO_SOURCE_CAMERA,
.controller = controller,
.fp = fp,
.kp = kp,
@@ -946,7 +949,7 @@ aoa_complete:
terminate_event_loop();
LOGD("quit...");
if (options->video_playback) {
if (options->window) {
// Close the window immediately on closing, because screen_destroy()
// may only be called once the video demuxer thread is joined (it may
// take time)

View File

@@ -144,64 +144,106 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
}
static void
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)) {
compute_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)) {
rect->x = 0;
rect->y = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
rect->w = render_size.width;
rect->h = render_size.height;
return;
}
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
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;
if (keep_width) {
rect->x = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.width * content_size.height
/ content_size.width;
rect->y = (drawable_size.height - rect->h) / 2;
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;
} else {
rect->y = 0;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
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;
}
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
// Only upscale video frames, not icon
bool can_upscale = screen->video;
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
compute_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->video);
assert(screen->has_video_window);
assert(!screen->video || screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation);
(void) res; // any error already logged
}
SDL_Renderer *renderer = screen->renderer;
sc_sdl_render_clear(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
SDL_FRect *geometry = &screen->rect;
enum sc_orientation orientation = screen->orientation;
SDL_Texture *texture = sc_texture_get_sdl_texture(&screen->tex);
if (!texture) {
return;
}
bool ok;
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());
}
sc_sdl_render_present(renderer);
}
#if defined(__APPLE__) || defined(_WIN32)
@@ -302,6 +344,7 @@ sc_screen_init(struct sc_screen *screen,
screen->orientation = SC_ORIENTATION_0;
screen->video = params->video;
screen->camera = params->camera;
screen->req.x = params->window_x;
screen->req.y = params->window_y;
@@ -368,13 +411,25 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
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();
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;
}
SDL_Surface *icon = sc_icon_load_scrcpy();
if (icon) {
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
@@ -385,24 +440,28 @@ sc_screen_init(struct sc_screen *screen,
} else {
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_window;
goto error_destroy_texture;
}
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 (!params->video) {
assert(icon);
screen->content_size.width = icon->w;
screen->content_size.height = icon->h;
ok = sc_texture_set_from_surface(&screen->tex, icon);
if (!ok) {
sc_icon_destroy(icon);
goto error_destroy_texture;
}
}
if (!ok) {
goto error_destroy_window;
if (icon) {
sc_icon_destroy(icon);
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_display;
goto error_destroy_texture;
}
struct sc_input_manager_params im_params = {
@@ -412,6 +471,7 @@ sc_screen_init(struct sc_screen *screen,
.kp = params->kp,
.mp = params->mp,
.gp = params->gp,
.camera = params->camera,
.mouse_bindings = params->mouse_bindings,
.legacy_paste = params->legacy_paste,
.clipboard_autosync = params->clipboard_autosync,
@@ -457,8 +517,10 @@ sc_screen_init(struct sc_screen *screen,
return true;
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_texture:
sc_texture_destroy(&screen->tex);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -520,8 +582,9 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_display_destroy(&screen->display);
sc_texture_destroy(&screen->tex);
av_frame_free(&screen->frame);
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
@@ -604,6 +667,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
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;
@@ -617,28 +681,12 @@ sc_screen_apply_frame(struct sc_screen *screen) {
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;
}
}
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
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) {
@@ -694,7 +742,10 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
sc_screen_apply_frame(screen);
bool ok = sc_screen_apply_frame(screen);
if (!ok) {
LOGE("Resume frame update failed");
}
}
if (!paused) {
@@ -776,14 +827,11 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
return false;
}
return true;
}
case SDL_EVENT_WINDOW_EXPOSED:
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
if (!screen->video || screen->has_video_window) {
sc_screen_render(screen, true);
}
return true;

View File

@@ -12,12 +12,12 @@
#include "controller.h"
#include "coords.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"
@@ -30,8 +30,9 @@ struct sc_screen {
#endif
bool video;
bool camera;
struct sc_display display;
struct sc_texture tex;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
@@ -48,6 +49,7 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@@ -59,7 +61,7 @@ struct sc_screen {
// client orientation
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_Rect rect;
struct SDL_FRect rect;
bool has_frame;
bool has_video_window;
@@ -71,6 +73,7 @@ struct sc_screen {
struct sc_screen_params {
bool video;
bool camera;
struct sc_controller *controller;
struct sc_file_pusher *fp;

View File

@@ -357,6 +357,13 @@ execute_server(struct sc_server *server,
if (params->camera_high_speed) {
ADD_PARAM("camera_high_speed=true");
}
if (params->camera_torch) {
ADD_PARAM("camera_torch=true");
}
if (params->camera_zoom) {
VALIDATE_STRING(params->camera_zoom);
ADD_PARAM("camera_zoom=%s", params->camera_zoom);
}
if (params->show_touches) {
ADD_PARAM("show_touches=true");
}

View File

@@ -35,6 +35,7 @@ struct sc_server_params {
const char *camera_id;
const char *camera_size;
const char *camera_ar;
const char *camera_zoom;
uint16_t camera_fps;
struct sc_port_range port_range;
uint32_t tunnel_host;
@@ -68,6 +69,7 @@ struct sc_server_params {
bool power_on;
bool kill_adb_on_close;
bool camera_high_speed;
bool camera_torch;
bool vd_destroy_content;
bool vd_system_decorations;
uint8_t list;

257
app/src/texture.c Normal file
View File

@@ -0,0 +1,257 @@
#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;
#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());
return false;
}
#endif
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) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(tex->gl_context);
#endif
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;
}
SDL_Texture *
sc_texture_get_sdl_texture(struct sc_texture *tex) {
return tex->texture;
}

54
app/src/texture.h Normal file
View File

@@ -0,0 +1,54 @@
#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"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
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;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
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);
SDL_Texture *
sc_texture_get_sdl_texture(struct sc_texture *tex);
#endif

View File

@@ -60,8 +60,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
SDL_Surface *icon = sc_icon_load_scrcpy();
if (icon) {
bool ok = SDL_SetWindowIcon(screen->window, icon);
if (!ok) {
@@ -77,7 +76,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
}
screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon);
scrcpy_icon_destroy(icon);
sc_icon_destroy(icon);
if (!screen->texture) {
goto error_destroy_renderer;
}

View File

@@ -5,6 +5,25 @@
#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();
@@ -25,24 +44,9 @@ sc_file_get_local_path(const char *name) {
*p = '\0'; // modify executable_path in place
char *dir = executable_path;
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);
char *file_path = sc_file_build_path(dir, name);
free(executable_path);
return file_path;
}

View File

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

@@ -130,6 +130,25 @@ 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,6 +34,9 @@ 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

@@ -446,6 +446,55 @@ static void test_serialize_reset_video(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_camera_set_torch(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
.camera_set_torch = {
.on = true,
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
0x01, // true
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_camera_zoom_in(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_camera_zoom_out(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@@ -470,5 +519,8 @@ int main(int argc, char *argv[]) {
test_serialize_open_hard_keyboard();
test_serialize_start_app();
test_serialize_reset_video();
test_serialize_camera_set_torch();
test_serialize_camera_zoom_in();
test_serialize_camera_zoom_out();
return 0;
}

View File

@@ -165,6 +165,30 @@ scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high
[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
## Torch
The camera torch can be turned on at startup by `--camera-torch`:
```
scrcpy --video-source=camera --camera-torch
```
It can also be turned on and off dynamically with <kbd>MOD</kbd>+<kbd>t</kbd>
and <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>, respectively.
## Zoom
The camera zoom can be set with `--camera-zoom=`:
```bash
scrcpy --video-source=camera --camera-zoom=1.5
```
It can also be adjusted dynamically using <kbd>MOD</kbd>+<kbd></kbd> _(up)_ and
<kbd>MOD</kbd>+<kbd></kbd> _(down)_.
## Webcam
Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera

View File

@@ -58,6 +58,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Tilt horizontally (slide with 2 fingers) | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
| Turn on the camera torch (camera mode only) | <kbd>MOD</kbd>+<kbd>t</kbd>
| Turn off the camera torch (camera mode only)| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>
| Zoom camera in (camera mode only) | <kbd>MOD</kbd>+<kbd></kbd> _(up)_
| Zoom camera out (camera mode only) | <kbd>MOD</kbd>+<kbd></kbd> _(down)_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._

View File

@@ -38,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/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$LINUX_BUILD_DIR/dist/"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

View File

@@ -38,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/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$MACOS_BUILD_DIR/dist/"
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"

2
run
View File

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

View File

@@ -44,8 +44,10 @@ public class Options {
private Size cameraSize;
private CameraFacing cameraFacing;
private CameraAspectRatio cameraAspectRatio;
private float cameraZoom = 1;
private int cameraFps;
private boolean cameraHighSpeed;
private boolean cameraTorch;
private boolean showTouches;
private boolean stayAwake;
private int screenOffTimeout = -1;
@@ -168,6 +170,10 @@ public class Options {
return cameraAspectRatio;
}
public float getCameraZoom() {
return cameraZoom;
}
public int getCameraFps() {
return cameraFps;
}
@@ -176,6 +182,10 @@ public class Options {
return cameraHighSpeed;
}
public boolean getCameraTorch() {
return cameraTorch;
}
public boolean getShowTouches() {
return showTouches;
}
@@ -469,12 +479,20 @@ public class Options {
options.cameraAspectRatio = parseCameraAspectRatio(value);
}
break;
case "camera_zoom":
if (!value.isEmpty()) {
options.cameraZoom = Float.parseFloat(value);
}
break;
case "camera_fps":
options.cameraFps = Integer.parseInt(value);
break;
case "camera_high_speed":
options.cameraHighSpeed = Boolean.parseBoolean(value);
break;
case "camera_torch":
options.cameraTorch = Boolean.parseBoolean(value);
break;
case "new_display":
options.newDisplay = parseNewDisplay(value);
break;

View File

@@ -25,6 +25,9 @@ public final class ControlMessage {
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final int TYPE_CAMERA_SET_TORCH = 18;
public static final int TYPE_CAMERA_ZOOM_IN = 19;
public static final int TYPE_CAMERA_ZOOM_OUT = 20;
public static final long SEQUENCE_INVALID = 0;
@@ -166,6 +169,13 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createCameraSetTorch(boolean on) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_CAMERA_SET_TORCH;
msg.on = on;
return msg;
}
public int getType() {
return type;
}

View File

@@ -47,6 +47,8 @@ public class ControlMessageReader {
case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate();
@@ -56,6 +58,8 @@ public class ControlMessageReader {
return parseUhidDestroy();
case ControlMessage.TYPE_START_APP:
return parseStartApp();
case ControlMessage.TYPE_CAMERA_SET_TORCH:
return parseCameraSetTorch();
default:
throw new ControlProtocolException("Unknown event type: " + type);
}
@@ -166,6 +170,11 @@ public class ControlMessageReader {
return ControlMessage.createStartApp(name);
}
private ControlMessage parseCameraSetTorch() throws IOException {
boolean on = dis.readBoolean();
return ControlMessage.createCameraSetTorch(on);
}
private Position parsePosition() throws IOException {
int x = dis.readInt();
int y = dis.readInt();

View File

@@ -12,7 +12,9 @@ import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VideoSource;
import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
@@ -75,6 +77,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private UhidManager uhidManager;
private final boolean camera;
private final int displayId;
private final boolean supportsInputEvents;
private final ControlChannel controlChannel;
@@ -97,13 +100,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean keepDisplayPowerOff;
// Used for resetting video encoding on RESET_VIDEO message
// Used for resetting video encoding on RESET_VIDEO message or for sending camera controls
private SurfaceCapture surfaceCapture;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.displayId = options.getDisplayId();
this.camera = options.getVideoSource() == VideoSource.CAMERA;
this.controlChannel = controlChannel;
this.cleanUp = cleanUp;
if (this.camera) {
// Unused for camera
this.displayId = Device.DISPLAY_ID_NONE;
this.supportsInputEvents = false;
this.sender = null;
this.clipboardAutosync = false;
this.powerOn = false;
return;
}
this.displayId = options.getDisplayId();
this.clipboardAutosync = options.getClipboardAutosync();
this.powerOn = options.getPowerOn();
initPointers();
@@ -201,7 +217,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException {
// on start, power on the device
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
if (!camera && powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack
@@ -236,7 +252,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}, "control-recv");
thread.start();
sender.start();
if (sender != null) {
sender.start();
}
}
@Override
@@ -244,7 +262,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (thread != null) {
thread.interrupt();
}
sender.stop();
if (sender != null) {
sender.stop();
}
}
@Override
@@ -252,90 +272,122 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (thread != null) {
thread.join();
}
sender.join();
if (sender != null) {
sender.join();
}
}
private boolean handleEvent() throws IOException {
ControlMessage msg;
try {
msg = controlChannel.recv();
} catch (ControlProtocolException e) {
Ln.e("Control protocol error", e);
return false;
} catch (IOException e) {
// this is expected on close
return false;
}
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
if (supportsInputEvents) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
}
break;
case ControlMessage.TYPE_INJECT_TEXT:
if (supportsInputEvents) {
injectText(msg.getText());
}
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (supportsInputEvents) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
if (supportsInputEvents) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
}
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (supportsInputEvents) {
pressBackOrTurnScreenOn(msg.getAction());
}
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel();
break;
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
break;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
getClipboard(msg.getCopyKey());
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
break;
case ControlMessage.TYPE_SET_DISPLAY_POWER:
if (supportsInputEvents) {
setDisplayPower(msg.getOn());
}
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(getActionDisplayId());
break;
case ControlMessage.TYPE_UHID_CREATE:
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
break;
case ControlMessage.TYPE_UHID_INPUT:
getUhidManager().writeInput(msg.getId(), msg.getData());
break;
case ControlMessage.TYPE_UHID_DESTROY:
getUhidManager().close(msg.getId());
break;
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
openHardKeyboardSettings();
break;
case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText());
break;
int type = msg.getType();
// Events for all sources (display or camera)
switch (type) {
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
return true;
default:
// do nothing
// fall through
}
return true;
if (!camera) {
switch (type) {
case ControlMessage.TYPE_INJECT_KEYCODE:
if (supportsInputEvents) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
}
return true;
case ControlMessage.TYPE_INJECT_TEXT:
if (supportsInputEvents) {
injectText(msg.getText());
}
return true;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (supportsInputEvents) {
injectTouch(
msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
}
return true;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
if (supportsInputEvents) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
}
return true;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (supportsInputEvents) {
pressBackOrTurnScreenOn(msg.getAction());
}
return true;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel();
return true;
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
return true;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels();
return true;
case ControlMessage.TYPE_GET_CLIPBOARD:
getClipboard(msg.getCopyKey());
return true;
case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
return true;
case ControlMessage.TYPE_SET_DISPLAY_POWER:
if (supportsInputEvents) {
setDisplayPower(msg.getOn());
}
return true;
case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(getActionDisplayId());
return true;
case ControlMessage.TYPE_UHID_CREATE:
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
return true;
case ControlMessage.TYPE_UHID_INPUT:
getUhidManager().writeInput(msg.getId(), msg.getData());
return true;
case ControlMessage.TYPE_UHID_DESTROY:
getUhidManager().close(msg.getId());
return true;
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
openHardKeyboardSettings();
return true;
case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText());
return true;
default:
// fall through
}
} else {
assert surfaceCapture instanceof CameraCapture;
CameraCapture cameraCapture = (CameraCapture) surfaceCapture;
switch (type) {
case ControlMessage.TYPE_CAMERA_SET_TORCH:
cameraCapture.setTorchEnabled(msg.getOn());
return true;
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
cameraCapture.zoomIn();
return true;
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
cameraCapture.zoomOut();
return true;
default:
// fall through
}
}
throw new AssertionError("Unexpected message type: " + type);
}
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
@@ -751,7 +803,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void resetVideo() {
if (surfaceCapture != null) {
Ln.i("Video capture reset");
surfaceCapture.requestInvalidate();
surfaceCapture.invalidate();
}
}
}

View File

@@ -23,6 +23,7 @@ import android.media.MediaCodecList;
import android.os.Build;
import android.util.Range;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -164,7 +165,7 @@ public final class LogUtils {
// Capture frame rates for low-FPS mode are the same for every resolution
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
if (lowFpsRanges != null) {
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
String uniqueLowFps = getFormattedUniqueSet(lowFpsRanges);
builder.append(", fps=").append(uniqueLowFps);
}
} catch (Exception e) {
@@ -172,6 +173,18 @@ public final class LogUtils {
Ln.w("Could not get available frame rates for camera " + id, e);
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_30_ANDROID_11) {
try {
Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
if (zoomRange != null) {
String zoom = getFormattedZoomRange(zoomRange);
builder.append(", zoom-range=").append(zoom);
}
} catch (Exception e) {
Ln.w("Could not get available zoom ranges for camera " + id, e);
}
}
builder.append(')');
if (includeSizes) {
@@ -191,7 +204,7 @@ public final class LogUtils {
builder.append("\n High speed capture (--camera-high-speed):");
for (android.util.Size size : highSpeedSizes) {
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
String uniqueHighFps = getFormattedUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
}
@@ -205,14 +218,31 @@ public final class LogUtils {
return builder.toString();
}
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
private static String getFormattedUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) {
set.add(range.getUpper());
}
return set;
StringBuilder builder = new StringBuilder("{");
boolean first = true;
for (Integer i : set) {
if (!first) {
builder.append(", ");
} else {
first = false;
}
builder.append(i);
}
builder.append("}");
return builder.toString();
}
private static String getFormattedZoomRange(Range<Float> range) {
DecimalFormat format = new DecimalFormat("#.##");
return "[" + format.format(range.getLower()) + ", " + format.format(range.getUpper()) + "]";
}
public static String buildAppListMessage() {
List<DeviceApp> apps = Device.listApps();

View File

@@ -36,6 +36,7 @@ import android.view.Surface;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@@ -53,6 +54,8 @@ public class CameraCapture extends SurfaceCapture {
0, 1, 0, 1, // column 4
};
private static final float ZOOM_FACTOR = 1 + 1 / 16f;
private final String explicitCameraId;
private final CameraFacing cameraFacing;
private final Size explicitSize;
@@ -63,10 +66,13 @@ public class CameraCapture extends SurfaceCapture {
private final Rect crop;
private final Orientation captureOrientation;
private final float angle;
private final boolean initialTorch;
private float zoom;
private String cameraId;
private Size captureSize;
private Size videoSize; // after OpenGL transforms
private Range<Float> zoomRange;
private AffineMatrix transform;
private OpenGLRunner glRunner;
@@ -78,6 +84,11 @@ public class CameraCapture extends SurfaceCapture {
private final AtomicBoolean disconnected = new AtomicBoolean();
// The following fields must be accessed only from the camera thread
private boolean started;
private CaptureRequest.Builder requestBuilder;
private CameraCaptureSession currentSession;
public CameraCapture(Options options) {
this.explicitCameraId = options.getCameraId();
this.cameraFacing = options.getCameraFacing();
@@ -90,6 +101,8 @@ public class CameraCapture extends SurfaceCapture {
this.captureOrientation = options.getCaptureOrientation();
assert captureOrientation != null;
this.angle = options.getAngle();
this.initialTorch = options.getCameraTorch();
this.zoom = options.getCameraZoom();
}
@Override
@@ -250,6 +263,7 @@ public class CameraCapture extends SurfaceCapture {
return ratio.getAspectRatio();
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
@Override
public void start(Surface surface) throws IOException {
if (transform != null) {
@@ -261,11 +275,68 @@ public class CameraCapture extends SurfaceCapture {
surface = glRunner.start(captureSize, videoSize, surface);
}
cameraHandler.post(() -> {
assertCameraThread();
started = true;
});
Surface captureSurface = surface;
OutputConfiguration outputConfig = new OutputConfiguration(captureSurface);
List<OutputConfiguration> outputs = Collections.singletonList(outputConfig);
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
assertCameraThread();
if (!started) {
// Stopped on the encoder thread between the call to start() and this callback
return;
}
CameraManager cameraManager = ServiceManager.getCameraManager();
try {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
} catch (CameraAccessException e) {
Ln.w("Could not get camera characteristics");
}
try {
requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
requestBuilder.addTarget(captureSurface);
if (fps > 0) {
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
}
if (initialTorch) {
Ln.i("Turn camera torch on");
requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
}
if (zoom != 1) {
zoom = clampZoom(zoom);
Ln.i("Set camera zoom: " + zoom);
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
}
CaptureRequest request = requestBuilder.build();
setRepeatingRequest(session, request);
currentSession = session;
} catch (CameraAccessException e) {
Ln.e("Camera error", e);
invalidate();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
Ln.e("Camera configuration error");
invalidate();
}
});
try {
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest request = createCaptureRequest(surface);
setRepeatingRequest(session, request);
} catch (CameraAccessException | InterruptedException e) {
cameraDevice.createCaptureSession(sessionConfig);
} catch (CameraAccessException e) {
stop();
throw new IOException(e);
}
@@ -273,6 +344,13 @@ public class CameraCapture extends SurfaceCapture {
@Override
public void stop() {
cameraHandler.post(() -> {
assertCameraThread();
currentSession = null;
requestBuilder = null;
started = false;
});
if (glRunner != null) {
glRunner.stopAndRelease();
glRunner = null;
@@ -353,46 +431,7 @@ public class CameraCapture extends SurfaceCapture {
}
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException {
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
OutputConfiguration outputConfig = new OutputConfiguration(surface);
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
future.complete(session);
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
}
});
camera.createCaptureSession(sessionConfig);
try {
return future.get();
} catch (ExecutionException e) {
throw (CameraAccessException) e.getCause();
}
}
private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException {
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
requestBuilder.addTarget(surface);
if (fps > 0) {
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
}
return requestBuilder.build();
}
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException {
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
@@ -419,8 +458,63 @@ public class CameraCapture extends SurfaceCapture {
return disconnected.get();
}
@Override
public void requestInvalidate() {
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
public void setTorchEnabled(boolean enabled) {
cameraHandler.post(() -> {
assertCameraThread();
if (currentSession != null && requestBuilder != null) {
try {
Ln.i("Turn camera torch " + (enabled ? "on" : "off"));
requestBuilder.set(CaptureRequest.FLASH_MODE, enabled ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);
CaptureRequest request = requestBuilder.build();
setRepeatingRequest(currentSession, request);
} catch (CameraAccessException e) {
Ln.e("Camera error", e);
}
}
});
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
private void zoom(boolean in) {
cameraHandler.post(() -> {
assertCameraThread();
if (currentSession != null && requestBuilder != null) {
// Always align to log values
double z = Math.round(Math.log(zoom) / Math.log(ZOOM_FACTOR));
double dir = in ? 1 : -1;
zoom = (float) Math.pow(ZOOM_FACTOR, z + dir);
try {
zoom = clampZoom(zoom);
Ln.i("Set camera zoom: " + zoom);
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
CaptureRequest request = requestBuilder.build();
setRepeatingRequest(currentSession, request);
} catch (CameraAccessException e) {
Ln.e("Camera error", e);
}
}
});
}
public void zoomIn() {
zoom(true);
}
public void zoomOut() {
zoom(false);
}
private float clampZoom(float value) {
assertCameraThread();
if (zoomRange == null) {
return value;
}
return zoomRange.clamp(value);
}
private void assertCameraThread() {
assert Thread.currentThread() == cameraThread;
}
}

View File

@@ -261,9 +261,4 @@ public class NewDisplayCapture extends SurfaceCapture {
int num = size.getMax();
return initialDpi * num / den;
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@@ -211,9 +211,4 @@ public class ScreenCapture extends SurfaceCapture {
SurfaceControl.closeTransaction();
}
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@@ -19,9 +19,9 @@ public abstract class SurfaceCapture {
private CaptureListener listener;
/**
* Notify the listener that the capture has been invalidated (for example, because its size changed).
* Notify the listener that the capture has been invalidated (for example, because its size changed, or due to a manual user request).
*/
protected void invalidate() {
public void invalidate() {
listener.onInvalidated();
}
@@ -86,11 +86,4 @@ public abstract class SurfaceCapture {
public boolean isClosed() {
return false;
}
/**
* Manually request to invalidate (typically a user request).
* <p>
* The capture implementation is free to ignore the request and do nothing.
*/
public abstract void requestInvalidate();
}

View File

@@ -422,6 +422,56 @@ public class ControlMessageReaderTest {
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseCameraSetTorch() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_CAMERA_SET_TORCH);
dos.writeBoolean(true);
byte[] packet = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_CAMERA_SET_TORCH, event.getType());
Assert.assertTrue(event.getOn());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseCameraZoomIn() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_IN);
byte[] packet = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_IN, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testParseCameraZoomOut() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_OUT);
byte[] packet = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
ControlMessageReader reader = new ControlMessageReader(bis);
ControlMessage event = reader.read();
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_OUT, event.getType());
Assert.assertEquals(-1, bis.read()); // EOS
}
@Test
public void testMultiEvents() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();