Compare commits

...

96 Commits
tmp ... docker

Author SHA1 Message Date
Romain Vimont
1ad2edc2d8 Dockerfile 2026-01-08 21:14:06 +01:00
dddddjent
dee1fd46a6 Fix SDL colorspace matching AVCOL_SPC_RGB
Use BT.709 color space for AVCOL_SPC_RGB.

Refs #1868 comment <https://github.com/Genymobile/scrcpy/issues/1868#issuecomment-3293917796>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-08 20:40:51 +01:00
Romain Vimont
a90a039f50 Upgrade SDL3 dependency list for Github Actions
Explicitly install required packages listed here:
<https://wiki.libsdl.org/SDL3/README-linux#build-dependencies>

Refs #6216 comment <https://github.com/Genymobile/scrcpy/pull/6216#issuecomment-3703680127>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
ab04e0348d Build SDL3 for test step on Github Actions
The Ubuntu version used to build scrcpy does not provide an SDL3
package.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
99b955f390 Upgrade SDL build script for SDL3
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
bbb855b5b0 Remove obsolete workaround for macOS
Refs #3031 <https://github.com/Genymobile/scrcpy/pull/3031>
Refs SDL/#5340 <https://github.com/libsdl-org/SDL/issues/5340>
Refs SDL/#5976 <https://github.com/libsdl-org/SDL/issues/5976>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
4879950a06 Fix asynchronous SDL window state handling
The SDL3 migration guide [1] says:

> The following window operations are now considered to be asynchronous
requests and should not be assumed to succeed unless a corresponding
event has been received […]

Remove the boolean fields used to track the window state, as they were
fundamentally inconsistent with SDL's internal state. Use
SDL_GetWindowFlags() explicitly instead.

[1]: <https://wiki.libsdl.org/SDL3/README-migration>

Refs #6216 comment <https://github.com/Genymobile/scrcpy/pull/6216#discussion_r2651810005>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
170b7a02e7 Disable "resize to pixel-perfect" when maximized
This improves consistency and will simplify further refactors.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:51 +01:00
Romain Vimont
9f1aac41a6 Check programming errors reported by SDL3
Refs #6216 comment <https://github.com/Genymobile/scrcpy/pull/6216#issuecomment-3399353701>
Refs SDL/#14223 <https://github.com/libsdl-org/SDL/issues/14223>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-08 20:40:48 +01:00
Romain Vimont
02989249f6 Migrate from SDL2 to SDL3
Refs <https://wiki.libsdl.org/SDL3/README-migration>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-06 16:22:48 +01:00
Romain Vimont
f8e0b9be4b Improve color space conversion
Use both the color space and color range from FFmpeg to determine the
appropriate SDL YUV conversion mode.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-04 14:14:33 +01:00
Romain Vimont
ed62ca124c Set color range during texture creation
This prepares for the migration to SDL3, where the color range can only
be specified at the time of texture creation.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-04 14:14:18 +01:00
Romain Vimont
3571fe62ed Create texture on first frame
The texture was created as soon as the initial video size was known,
even before the first frame arrived.

However, texture creation will require other data, such as the color
range, which is only available once the first frame is received.

Therefore, delay texture creation until the first frame.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-04 14:13:33 +01:00
Romain Vimont
bca98133d1 Remove prepare_for_frame()
Inline the function body into its only caller to simplify further
refactors.

PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-04 14:12:12 +01:00
Romain Vimont
881c71b2e8 Handle mouse integer scrolling locally
Do not rely on SDL to expose integer scroll values, as they are not
available in all versions.

It was reimplemented in SDL 3.2.12 by this commit:
<0447c2f3c3>

Refs #6156 <https://github.com/Genymobile/scrcpy/issues/6156>
PR #6216 <https://github.com/Genymobile/scrcpy/pull/6216>
2026-01-04 14:11:51 +01:00
Romain Vimont
09eed565fc Remove unnecessary dependencies for Windows builds
The GitHub Actions script installed unnecessary packages when
cross-building for Windows.
2026-01-04 14:04:57 +01:00
Romain Vimont
b0da401e6d Fix documentation of input_events.h
Add missing reference to sc_gamepad_processor.

Refs #6216 comment <https://github.com/Genymobile/scrcpy/pull/6216#pullrequestreview-3616928335>
2026-01-04 14:04:34 +01:00
Romain Vimont
dba2a3778f Include USB header only if HAVE_USB 2026-01-02 14:59:10 +01:00
Romain Vimont
cda4387058 Add missing break statement
Refs #6439 comment <https://github.com/Genymobile/scrcpy/pull/6439#issuecomment-3705283406>
2026-01-02 14:47:28 +01:00
Romain Vimont
42632d3f53 Drop root privileges on startup
On rooted devices, switch to user 2000 during startup.

Copy-paste does not work as root (user 0), so switching to 2000 fixes
the issue.

Fixes #6224 <https://github.com/Genymobile/scrcpy/issues/6224>

Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2025-12-30 14:40:53 +01:00
Romain Vimont
db2f72a58a Merge branch 'master' into dev 2025-12-17 20:27:11 +01:00
Romain Vimont
fb6381f5b9 Upgrade links to 3.3.4 2025-12-17 20:21:05 +01:00
Romain Vimont
06fd3b4786 Bump version to 3.3.4 2025-12-17 19:57:50 +01:00
Romain Vimont
1d1a4103cc Replace macos-13 runner with macos-15-intel
Refs <https://github.com/actions/runner-images/issues/13046>
Refs #5526 <https://github.com/Genymobile/scrcpy/pull/5526>
2025-12-17 19:57:50 +01:00
Romain Vimont
5b51396a8c Fix permission denial error after Android upgrade
Assign the FakeContext instance to ActivityManager.mContext to avoid a
permission error:

    Permission Denial: package=android does not belong to uid=2000

Fixes #6523 <https://github.com/Genymobile/scrcpy/issues/6523>
2025-12-09 20:24:42 +01:00
Romain Vimont
7e66062086 Extract function to execute code on a handler
Extract function to synchronously execute code on a handler, waiting for
the execution to complete.
2025-11-28 22:42:17 +01:00
Romain Vimont
6f9eb31d52 Add missing test for START_APP serialization
A test for Java deserialization of the START_APP control message was
already present, but the corresponding C-side serialization test was
missing.

Refs 13ce277e1f
2025-11-25 00:13:13 +01:00
Romain Vimont
9cfa5b197a Create Application instance via instrumentation
This fixes an issue on certain Meizu devices.

Fixes #6480 <https://github.com/Genymobile/scrcpy/issues/6480>
2025-11-23 15:56:31 +01:00
paradoxskin
b08093d1c0 Fix incorrect icon filename in build documentation
The installed icon was listed as `icon.png`, but the actual
filename is `scrcpy.png`.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-11-19 17:13:40 +01:00
valord577
7dd9bcaf60 Prevent error log interleaving
Between the calls to CONSOLE_ERR.print() and
printStackTrace(CONSOLE_ERR), logs from other threads may be inserted.

Synchronizing access to CONSOLE_ERR ensures that logs from different
threads do not mix.

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

Signed-off-by: valord577 <valord577@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-11-18 11:17:17 +01:00
Romain Vimont
3530851071 Fix uncaught exception handler
The default handler was mistakenly retrieved after our custom handler
was set, causing it to reference itself. As a result, this led to
infinite recursion.

Bug introduced by eee3f24739.
2025-11-04 20:35:39 +01:00
Romain Vimont
d0047b2110 Do not fail when uniqueId field is missing
On some devices, DisplayInfo does not have a "uniqueId" field. This
field is only used for correct UHID behavior on virtual displays, so its
absence should not prevent scrcpy from working.

Refs #6009 <https://github.com/Genymobile/scrcpy/pull/6009>
Fixes #6461 <https://github.com/Genymobile/scrcpy/issues/6461>
2025-11-01 00:34:20 +01:00
Romain Vimont
3281fda6ef Set URL explicitly in dependency build scripts
Explicitly set the URL of each dependency at the beginning of its
script.

PROJECT_DIR and FILENAME are internal details.
2025-10-30 22:31:16 +01:00
Romain Vimont
925949d54a Refactor dependency build scripts initialization
Rename "common" to "_init" because it not only exposes common functions
but also initializes environment variables.

Call _init in a single line in all dependency build scripts.
2025-10-30 22:27:17 +01:00
Yan
f3d4fde15b Fix handling of non-integer ANDROID_PLATFORM
ANDROID_PLATFORM is not always an integer; it can also be a value like
"36.1". Handle such cases properly.

This fixes the following error:

    server/build_without_gradle.sh: line 89:
    [[: 36.1: syntax error: invalid arithmetic operator (error token is ".1")

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-10-19 21:00:05 +02:00
Romain Vimont
eee3f24739 Upgrade Gradle and use Android SDK 36 2025-10-19 21:00:05 +02:00
Romain Vimont
3e40b24737 Fix UHID_OUTPUT message parsing
The bounds check was incorrect.

Fixes #6415 <https://github.com/Genymobile/scrcpy/issues/6415>
2025-10-09 09:35:14 +02:00
Romain Vimont
9d7a4c88e0 Apply workarounds in the cleanup process
Accessing settings may require workarounds on certain devices.

Fixes #6405 <https://github.com/Genymobile/scrcpy/issues/6405>
2025-10-05 21:36:10 +02:00
Romain Vimont
e5e58b1b30 Upgrade links to 3.3.3 2025-09-27 16:10:10 +02:00
Romain Vimont
10a0974f43 Bump version to 3.3.3 2025-09-23 21:18:45 +02:00
Romain Vimont
be21e43be5 Fix frame leak on pending frame update
The previous pending frame was not unreferenced before referencing the
new one, causing frames to leak whenever a texture update failed
(typically on Windows when the window is minimized with D3D9).

Refs 6298ef095f
Fixes #4297 <https://github.com/Genymobile/scrcpy/issues/4297>
Fixes #6357 <https://github.com/Genymobile/scrcpy/issues/6357>
2025-09-23 21:18:45 +02:00
Romain Vimont
bfb0872493 Avoid resetting pending frame
The function update_texture() calls update_texture_internal() and falls
back to set_pending_frame() if it fails.

When the frame passed is the pending frame, call only the _internal()
version instead.

This will prevent issues with frame reference counts by ensuring the
source and destination frames are never the same.

Refs 6298ef095f
Refs #6357 <https://github.com/Genymobile/scrcpy/issues/6357>
2025-09-23 21:18:08 +02:00
Romain Vimont
e11399aff0 Ignore unknown methods in IDisplayWindowListener
New Android versions may add methods to IDisplayWindowListener.aidl.
When these methods are called by the system, they result in an
AbstractMethodError because they are not implemented on the scrcpy side.

To avoid releasing a new version for each newly added method, ignore
them at the Binder level.

Refs afaca80b37
Fixes #6362 <https://github.com/Genymobile/scrcpy/issues/6362>
2025-09-18 10:29:42 +02:00
David Griswold
9d56d26d45 Make virtual display presentable
With this flag, apps with baked in two-screen support can see the
virtual display as an external display they can present to.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-09-11 21:02:13 +02:00
Romain Vimont
f663bbec12 Update links to 3.3.2 2025-09-06 14:54:36 +02:00
Romain Vimont
2506d1768b Bump version to 3.3.2 2025-09-06 14:36:37 +02:00
Romain Vimont
4ee94cb845 Workaround clipboard issue on Samsung devices
Fixes #6224 <https://github.com/Genymobile/scrcpy/issues/6224>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2025-09-06 14:36:33 +02:00
Romain Vimont
afaca80b37 Fix virtual display after Android 16 upgrade
Several methods have been added upstream to IDisplayWindowListener.aidl,
causing an AbstractMethodError when they are called on the listener
instance implemented by scrcpy.

Fixes #6234 <https://github.com/Genymobile/scrcpy/issues/6234>
Fixes #6331 <https://github.com/Genymobile/scrcpy/issues/6331>
2025-09-06 14:19:43 +02:00
Filip Buda
8057835a0d Catch CTRL_BREAK_EVENT signal on Windows
This ensures the process can terminate properly when a CTRL_BREAK_EVENT
signal is sent programmatically.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-08-12 18:17:32 +02:00
Yan
e47529ab9c Fix gl_context declared type
The field gl_context is initialized from SDL_GL_CreateContext(), which
returns a raw SDL_GLContext, not a pointer.

The type mismatch was silently ignored by SDL2 because SDL_GLContext
was defined as an alias to `void *` (in SDL3, it is instead an alias to
`struct SDL_GLContextState *`, so compilation fails).

Refs #3895 <https://github.com/Genymobile/scrcpy/pull/3895>
PR #6259 <https://github.com/Genymobile/scrcpy/pull/6259>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-08-12 18:08:22 +02:00
Romain Vimont
939c8e7f68 Simplify settings access
For Android >= 12, scrcpy executed "settings" commands (in a new
process) rather than using the ContentProvider directly, due to
permission issues [1].

However, these permission issues were resolved by introducing
FakeContext.getContentResolver() [2].

Therefore, remove the use of "settings" commands and use the
ContentProvider directly in all cases.

Refs [1] cc0902b13c
Refs [2] 91373d906b
Refs #6224 comment <https://github.com/Genymobile/scrcpy/issues/6224#issuecomment-3078418268>
2025-07-17 18:27:21 +02:00
Romain Vimont
eb576c44f8 Replace __WINDOWS__ by _WIN32
Replace the SDL2-specific preprocessor macro __WINDOWS__ by the
"standard" _WIN32 macro.
2025-07-17 18:23:25 +02:00
Romain Vimont
0522d02d40 Add missing includes
The headers were implicitly included recursively, but include them
explicitly.
2025-07-17 18:23:12 +02:00
Romain Vimont
30bfc80f9b Fix style for 80-char limit 2025-07-17 13:20:45 +02:00
Romain Vimont
c3d2ef1b1f Remove redundant ninja install for GA macOS runner
The ninja package is already installed, so this triggered a warning:

> ninja 1.13.0 is already installed and up-to-date. To reinstall 1.13.0,
> run: brew reinstall ninja
2025-07-17 13:20:45 +02:00
Romain Vimont
a79ddc35a7 Update platform-tools checksums
The release binaries of platform-tools_r36.0.0 have changed upstream.

Both releases versions are referenced from
<https://dl.google.com/android/repository/repository2-2.xml>

Refs #6214 <https://github.com/Genymobile/scrcpy/issues/6214>
Refs <https://issuetracker.google.com/issues/431119334>
2025-07-17 13:20:45 +02:00
Romain Vimont
04542a9f58 Fix window leak on icon error 2025-07-17 13:19:15 +02:00
Romain Vimont
8761dcb7a8 Fix SDL dependency script error message
Commit 360936248c mistakenly left an
additional 'H' when replacing $HOST with $DIRNAME.

Refs <https://github.com/Genymobile/scrcpy/pull/6216#issuecomment-3076069802>
2025-07-16 19:09:47 +02:00
Romain Vimont
f01231dff8 Update links to 3.3.1 2025-06-20 20:14:42 +02:00
Romain Vimont
5b18ce0d2e Bump version to 3.3.1 2025-06-20 19:54:16 +02:00
Romain Vimont
4841fdd1ef Add horizontal scrolling support for HID mouse
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
fc75319bb2 Fix HID mouse support with SDL precise scrolling
Over HID, only integral scroll values can be sent. When SDL precise
scrolling is active, scroll events may include fractional values (e.g.,
0.05), which are truncated to 0 in the HID event.

To fix the problem, use the integral scroll value reported by SDL, which
internally accumulates fractional deltas.

Fixes #6156 <https://github.com/Genymobile/scrcpy/issues/6156>
PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
7c8bdccbdc Extend value range for SDK mouse scrolling
SDL precise scrolling can sometimes produce values greater than 1 or
less than -1.

On the wire, the value is encoded as a 16-bit fixed-point number.

Previously, the range was interpreted as [-1, 1], using 1 bit for the
integral part (the sign) and 15 bits for the fractional part.

To support larger values, interpret the range as [-16, 16] instead,
using 5 bits for the integral part and 11 bits for the fractional part
(which is more than enough).

PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 19:54:16 +02:00
Romain Vimont
9787fe5d26 Preserve original scroll values in mouse event
Clamp scroll values to [-1, 1] only for the SDK mouse.

HID mouse implementations perform their own clamping to [-127, 127] (in
hid_mouse.c).

PR #6172 <https://github.com/Genymobile/scrcpy/pull/6172>
2025-06-20 18:30:50 +02:00
Romain Vimont
98d30288f7 Prepare the main looper earlier
The looper must be initialized before listing apps, to avoid the
following error:

> Can't create handler inside thread that has not called
> Looper.prepare()

Refs 283326b2f6
Fixes #6165 <https://github.com/Genymobile/scrcpy/issues/6165>
2025-06-17 21:08:54 +02:00
Romain Vimont
d74cfd5711 Silence DiscouragedPrivateApi lint warning 2025-06-13 09:40:45 +02:00
Romain Vimont
cd3a5d50b6 Create ClipboardManager from the main thread
The ClipboardManager is instantiated by the first call to
ServiceManager.getClipboardManager().

Now that scrcpy uses android.content.ClipboardManager directly, it must
ensure that it is created on the main thread (or at least on a thread
with a Looper), to avoid the following error:

> Can't create handler inside thread that has not called
> Looper.prepare()

Refs 8a02e3c2f5
Fixes #6151 <https://github.com/Genymobile/scrcpy/issues/6151>
2025-06-13 09:37:32 +02:00
Romain Vimont
772f42134a Use Context.CLIPBOARD_SERVICE directly
The constant is defined in Context, not FakeContext.
2025-06-13 09:37:32 +02:00
berk ziya
38256d8ff9 Fix deprecated brew command
`brew cask` is an outdated command, replaced by `brew install --cask`.

Refs #5398 <https://github.com/Genymobile/scrcpy/pull/5398>
PR #6149 <https://github.com/Genymobile/scrcpy/pull/6149>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-12 15:30:04 +02:00
Romain Vimont
4e1cf13a50 Run a main looper in the cleanup process
Since a main looper is explicitly run in the main process, the
initialization of workarounds no longer calls
Looper.prepareMainLooper(), leading to a crash:

    java.lang.RuntimeException: Can't create handler inside thread
    Thread[main,5,main] that has not called Looper.prepare()

As a result, --power-off-on-close was broken.

Refs 283326b2f6
Fixes #6146 <https://github.com/Genymobile/scrcpy/issues/6146>
2025-06-12 09:10:11 +02:00
Romain Vimont
696402c68c Update links to 3.3 2025-06-11 22:15:30 +02:00
Romain Vimont
dc169e425e Bump version to 3.3 2025-06-11 19:39:48 +02:00
Romain Vimont
13fc75902a Merge branch 'master' into release 2025-06-11 19:39:09 +02:00
Romain Vimont
454beaa757 Upgrade libusb (1.0.29) 2025-06-11 19:39:02 +02:00
Romain Vimont
1a9ffb3814 Upgrade SDL (2.32.8) 2025-06-11 19:38:29 +02:00
Romain Vimont
ac16be54c8 Upgrade platform-tools (36.0.0) 2025-06-11 19:36:22 +02:00
Romain Vimont
8a02e3c2f5 Simplify ClipboardManager wrapper
Use the public ClipboardManager API, with the FakeContext as context.

This requires a running main looper, otherwise clipboard changes are not
processed.

Refs #6009 <https://github.com/Genymobile/scrcpy/pull/6009>
PR #6129 <https://github.com/Genymobile/scrcpy/pull/6129>

Suggested by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2025-06-11 17:47:37 +02:00
Romain Vimont
283326b2f6 Run a main looper
Instead of blocking the main thread until completion, run a looper.

This will allow the main thread to process any event posted to the main
looper.

Refs #6009 comment <https://github.com/Genymobile/scrcpy/pull/6009#issuecomment-2940810736>
PR #6129 <https://github.com/Genymobile/scrcpy/pull/6129>
2025-06-11 17:47:07 +02:00
Simon Chan
ca4f50c5ef Associate UHID devices to virtual displays
This allows the mouse pointer to appear on the correct display (only for
devices running Android 15+).

Fixes #5547 <https://github.com/Genymobile/scrcpy/issues/5547>
PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 20:34:11 +02:00
Romain Vimont
7a3fe830d4 Synchronize access to DisplayManager
The DisplayManager and its method getDisplayInfo() may be used from both
the Controller thread and the video (main) thread.

PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>
2025-06-05 20:34:11 +02:00
Romain Vimont
ee414231ed Cache getDisplayInfo method
Do not use reflection to retrieve the method for every call.

PR #6009 <https://github.com/Genymobile/scrcpy/pull/6009>
2025-06-05 20:34:11 +02:00
Simon Chan
41ed40f5f9 Simplify InputManager wrapper
Use the public InputManager API.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 20:34:11 +02:00
Colin Kinloch
d2cc930975 Add app name SDL hint
This allows pulseaudio to label the audio stream "scrcpy" rather than
"SDL Application".

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-06-05 19:59:59 +02:00
Romain Vimont
52f5d08d1f Avoid calling wait(0)
Calling wait(0) results in waiting without a timeout, which is
unintended.

Refs #6009 comment <https://github.com/Genymobile/scrcpy/pull/6009#issuecomment-2935930294>
2025-06-03 21:15:11 +02:00
Romain Vimont
70bfa2cf39 Remove useless flag in zsh completion script
The -N flag is only useful after a pattern section (-p) to switch back
to listing command names.

Refs <https://zsh.sourceforge.io/Doc/Release/Completion-System.html>
2025-05-22 20:00:58 +02:00
hltdev8642
38f779d9d3 Escape parentheses in zsh completion script
PR #6079 <https://github.com/Genymobile/scrcpy/pull/6079>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-05-22 20:00:53 +02:00
Romain Vimont
8cd63cb63e Report specific error for INJECT_EVENT permission
Some devices require a specific option to be enabled in Developer
Options to avoid a permission issue when injecting input events.

When this error occurs, hide the stack trace and print a human-readable
message explaining how to fix the issue.

PR #6080 <https://github.com/Genymobile/scrcpy/pull/6080>
2025-05-15 19:52:52 +02:00
Romain Vimont
cc309a2b34 Build static linux binary on Ubuntu 22.04
Ubuntu 20.04 is no longer available on GitHub Actions.

Refs <https://github.com/actions/runner-images/issues/11101>
Refs #6050 <https://github.com/Genymobile/scrcpy/pull/6050>

This reverts commit 69858c6f43.
2025-05-02 11:39:47 +02:00
Romain Vimont
91a4a74641 Move regex pattern initialization
If text == null, then the Pattern is not used.
2025-04-25 10:24:07 +02:00
Romain Vimont
48f38c4bb6 Fix default locked capture orientation
The default landscape locked orientation was reversed.

Fixes #6010 <https://github.com/Genymobile/scrcpy/issues/6010>
2025-04-24 16:12:28 +02:00
Romain Vimont
6875e9aa88 Revert "Fix AudioRecord package name for Android 16"
This reverts commit c27d116a66.

This commit breaks audio on Android 16 beta 4.

Refs #5960 comment <https://github.com/Genymobile/scrcpy/issues/5960#issuecomment-2816608015>
Fixes #6021 <https://github.com/Genymobile/scrcpy/issues/6021>
2025-04-24 16:05:13 +02:00
Nicholas Wilson
c5ed2cfc28 Replace "licence" with "license" in README
Although "licence" is correct in British English, the rest of the
statement uses "license," so change it for consistency.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2025-04-19 10:30:29 +02:00
Romain Vimont
1a0d300786 Add missing --screen-off-timeout doc in manpage
Refs eff5b4b219
2025-04-14 18:07:37 +02:00
Romain Vimont
d2447b5c19 Fix --screen-off-timeout bash completion
Only the option must be auto-completed, not its value.
2025-04-14 18:05:08 +02:00
Romain Vimont
5900e9e39c Remove irrelevant link in FAQ 2025-04-07 10:30:56 +02:00
Romain Vimont
882003f314 Fix segfault on SDL event without window
Since #5804, controls have been enabled even with --no-window. As a
result, the Android clipboard is synchronized with the computer, causing
SDL to trigger an SDL_CLIPBOARDUPDATE event.

This event is ignored by scrcpy, but it was still transmitted to the
sc_screen instance, even if it had not been initialized.

Fix the issue by calling sc_screen_handle_event() only when a screen
instance exists.

Refs #5804 <https://github.com/Genymobile/scrcpy/pull/5804>
Fixes #5970 <https://github.com/Genymobile/scrcpy/issues/5970>
2025-04-03 08:15:55 +02:00
99 changed files with 1813 additions and 1454 deletions

View File

@@ -72,19 +72,33 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
# Same as build-linux-x86_64
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
# SDL3 is not available in Ubuntu yet
- name: Install SDL3
run: |
app/deps/sdl.sh linux native shared
- name: Test
run: release/test_client.sh
run: |
export PKG_CONFIG_PATH="$PWD"/app/deps/work/install/linux-native-shared/lib/pkgconfig
export LD_LIBRARY_PATH="$PWD"/app/deps/work/install/linux-native-shared/lib
release/test_client.sh
build-linux-x86_64:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Check architecture
run: |
@@ -98,13 +112,19 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
libasound2-dev libpulse-dev \
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
- name: Build
run: release/build_linux.sh x86_64
@@ -132,9 +152,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
sudo apt install -y meson ninja-build nasm \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Build
@@ -163,9 +181,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
sudo apt install -y meson ninja-build nasm \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Build
@@ -202,8 +218,7 @@ jobs:
- name: Install dependencies
run: |
brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
brew install meson nasm libiconv zlib automake autoconf libtool
- name: Build
env:
@@ -230,7 +245,7 @@ jobs:
path: release/work/build-macos-aarch64/dist-tar/
build-macos-x86_64:
runs-on: macos-13
runs-on: macos-15-intel
steps:
- name: Check architecture
run: |
@@ -245,7 +260,7 @@ jobs:
uses: actions/checkout@v4
- name: Install dependencies
run: brew install meson ninja nasm libiconv zlib automake
run: brew install meson nasm libiconv zlib automake
# autoconf and libtool are already installed on macos-13
- name: Build

5
FAQ.md
View File

@@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set.
### Special characters do not work
The default text injection method is [limited to ASCII characters][text-input].
A trick allows to also inject some [accented characters][accented-characters],
The default text injection method is limited to ASCII characters. A trick allows
to also inject some [accented characters][accented-characters],
but that's all. See [#37].
To avoid the problem, [change the keyboard mode to simulate a physical
keyboard][hid].
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: doc/keyboard.md#physical-keyboard-simulation

View File

@@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v3.2)
# scrcpy (v3.3.4)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
On some devices (especially Xiaomi), you might get the following error:
```
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
```
In that case, you need to enable [an additional option][control] `USB debugging
@@ -129,7 +129,7 @@ Here are just some common examples.
scrcpy --otg
```
- Control the device using gamepad controllers plugged into the computer:
- Control the device using gamepads plugged into the computer:
```bash
scrcpy --gamepad=uhid
@@ -207,7 +207,7 @@ work][donate]:
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work
## Licence
## License
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2025 Romain Vimont

View File

@@ -205,6 +205,7 @@ _scrcpy() {
|-p|--port \
|--push-target \
|--rotation \
|--screen-off-timeout \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \

View File

@@ -1,4 +1,4 @@
#compdef -N scrcpy -N scrcpy.exe
#compdef scrcpy scrcpy.exe
#
# name: scrcpy
# auth: hltdev [hltdev8642@gmail.com]
@@ -11,7 +11,7 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-buffer=[Configure the audio buffering delay \(in milliseconds\)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]'
@@ -35,10 +35,10 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
'-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]'
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
{-h,--help}'[Print the help]'
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
'-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]'
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
@@ -48,7 +48,7 @@ arguments=(
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]'
'--max-fps=[Limit the frame rate of screen capture]'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]'

View File

@@ -1,10 +1,9 @@
#!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed
process_args() {
if [[ $# != 3 ]]
then
# <host>: win32 or win64
# <host>: linux, macos, win32 or win64
# <build_type>: native or cross
# <link_type>: static or shared
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
@@ -12,8 +11,8 @@ process_args() {
fi
HOST="$1"
BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" # static or shared
BUILD_TYPE="$2"
LINK_TYPE="$3"
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
VERSION=36.0.0
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-linux.zip"
SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8
PROJECT_DIR="platform-tools-$VERSION-linux"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR"
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
VERSION=36.0.0
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-darwin.zip"
SHA256SUM=d3e9fa1df3345cf728586908426615a60863d2632f73f1ce14f0f1349ef000fd
PROJECT_DIR="platform-tools-$VERSION-darwin"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR"
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init "$@"
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
VERSION=36.0.0
URL="https://dl.google.com/android/repository/platform-tools_r$VERSION-win.zip"
SHA256SUM=12c2841f354e92a0eb2fd7bf6f0f9bf8538abce7bd6b060ac8349d6f6a61107c
PROJECT_DIR="platform-tools-$VERSION-windows"
FILENAME="$PROJECT_DIR.zip"
cd "$SOURCES_DIR"
@@ -15,7 +15,7 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=1.5.0
FILENAME=dav1d-$VERSION.tar.gz
PROJECT_DIR=dav1d-$VERSION
URL="https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/dav1d-$VERSION.tar.gz"
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
PROJECT_DIR="dav1d-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=7.1.1
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
URL="https://ffmpeg.org/releases/ffmpeg-$VERSION.tar.xz"
SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1
PROJECT_DIR="ffmpeg-$VERSION"
FILENAME="$PROJECT_DIR.tar.xz"
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=1.0.28
FILENAME=libusb-$VERSION.tar.gz
PROJECT_DIR=libusb-$VERSION
SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe
VERSION=1.0.29
URL="https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz"
SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74
PROJECT_DIR="libusb-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR"
@@ -16,7 +16,7 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi

View File

@@ -1,33 +0,0 @@
From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001
From: Neal Gompa <neal@gompa.dev>
Date: Mon, 10 Feb 2025 05:00:56 -0500
Subject: [PATCH] pipewire: Ensure that the correct struct is used for
enumeration APIs
PipeWire now requires the correct struct type is used, otherwise
it will fail to compile.
Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f
Fixes: https://github.com/libsdl-org/SDL/issues/12224
(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6)
---
src/audio/pipewire/SDL_pipewire.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c
index 889e05decb..5d1bfc28de 100644
--- a/src/audio/pipewire/SDL_pipewire.c
+++ b/src/audio/pipewire/SDL_pipewire.c
@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info)
/* Need to parse the parameters to get the sample rate */
for (i = 0; i < info->n_params; ++i) {
- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL);
+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL);
}
hotplug_core_sync(node);
--
2.49.0

View File

@@ -1,14 +1,14 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=2.32.2
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4
VERSION=3.4.0
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
SHA256SUM=9614b9696abc4597ffce6b888829dc6537ae500423474c342ac4a67222c5654c
PROJECT_DIR="sdl-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
cd "$SOURCES_DIR"
@@ -16,9 +16,9 @@ if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch
get_file "$URL" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "SDL-release-$VERSION"
mv "SDL-release-$VERSION" "$PROJECT_DIR"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
@@ -29,52 +29,56 @@ export CXXFLAGS="$CFLAGS"
if [[ -d "$DIRNAME" ]]
then
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
mkdir "$DIRNAME"
cd "$DIRNAME"
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/$DIRNAME"
-DSDL_TESTS=OFF
)
if [[ "$HOST" == linux ]]
then
conf+=(
--enable-video-wayland
--enable-video-x11
-DSDL_WAYLAND=ON
-DSDL_X11=ON
)
fi
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
-DBUILD_SHARED_LIBS=OFF
)
else
conf+=(
--disable-static
--enable-shared
-DBUILD_SHARED_LIBS=ON
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
if [[ "$HOST" = win32 ]]
then
TOOLCHAIN_FILENAME="cmake-toolchain-mingw64-i686.cmake"
elif [[ "$HOST" = win64 ]]
then
TOOLCHAIN_FILENAME="cmake-toolchain-mingw64-x86_64.cmake"
else
echo "Unsupported cross-build to host: $HOST" >&2
exit 1
fi
conf+=(
--host="$HOST_TRIPLET"
-DCMAKE_TOOLCHAIN_FILE="$SOURCES_DIR/$PROJECT_DIR/build-scripts/$TOOLCHAIN_FILENAME"
)
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
cmake "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}"
fi
make -j
# There is no "make install-strip"
make install
# Strip manually
if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]]
then
${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll"
fi
cmake --build .
cmake --install .

View File

@@ -57,6 +57,7 @@ src = [
'src/util/process.c',
'src/util/process_intr.c',
'src/util/rand.c',
'src/util/sdl.c',
'src/util/strbuf.c',
'src/util/str.c',
'src/util/term.c',
@@ -117,7 +118,7 @@ dependencies = [
dependency('libavcodec', version: '>= 57.37', static: static),
dependency('libavutil', static: static),
dependency('libswresample', static: static),
dependency('sdl2', version: '>= 2.0.5', static: static),
dependency('sdl3', version: '>= 3.2.0', static: static),
]
if v4l2_support
@@ -275,7 +276,7 @@ if get_option('buildtype') == 'debug'
exe = executable(t[0], sources,
include_directories: src_dir,
dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
c_args: ['-DSC_TEST'])
test(t[0], exe)
endforeach
endif

View File

@@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "3.2"
VALUE "ProductVersion", "3.3.4"
END
END
BLOCK "VarFileInfo"

View File

@@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.B "\-\-screen\-off\-timeout " seconds
Set the screen off timeout while scrcpy is running (restore the initial value on exit).
.TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".

View File

@@ -103,14 +103,14 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
static void
show_adb_installation_msg(void) {
#ifndef __WINDOWS__
#ifndef _WIN32
static const struct {
const char *binary;
const char *command;
} pkg_managers[] = {
{"apt", "apt install adb"},
{"apt-get", "apt-get install adb"},
{"brew", "brew cask install android-platform-tools"},
{"brew", "brew install --cask android-platform-tools"},
{"dnf", "dnf install android-tools"},
{"emerge", "emerge dev-util/android-tools"},
{"pacman", "pacman -S android-tools"},
@@ -331,7 +331,7 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
bool
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags) {
#ifdef __WINDOWS__
#ifdef _WIN32
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
@@ -351,7 +351,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef __WINDOWS__
#ifdef _WIN32
free((void *) remote);
free((void *) local);
#endif
@@ -362,7 +362,7 @@ sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) {
#ifdef __WINDOWS__
#ifdef _WIN32
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
@@ -377,7 +377,7 @@ sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef __WINDOWS__
#ifdef _WIN32
free((void *) local);
#endif

View File

@@ -1,23 +1,40 @@
#include "audio_player.h"
#include "util/log.h"
#include "SDL3/SDL_hints.h"
/** Downcast frame_sink to sc_audio_player */
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
#define SC_SDL_SAMPLE_FMT AUDIO_F32
#define SC_SDL_SAMPLE_FMT SDL_AUDIO_F32LE
static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
sc_audio_player_stream_callback(void *userdata, SDL_AudioStream *stream,
int additional_amount, int total_amount) {
(void) total_amount;
struct sc_audio_player *ap = userdata;
assert(len_int > 0);
size_t len = len_int;
size_t len = additional_amount;
assert(len % ap->audioreg.sample_size == 0);
uint32_t out_samples = len / ap->audioreg.sample_size;
sc_audio_regulator_pull(&ap->audioreg, stream, out_samples);
// The requested amount may exceed the internal aout_buffer size.
// In this (unlikely) case, send the data to the stream in multiple chunks.
while (len) {
size_t chunk_size = MIN(ap->aout_buffer_size, len);
uint32_t out_samples = chunk_size / ap->audioreg.sample_size;
sc_audio_regulator_pull(&ap->audioreg, ap->aout_buffer,
out_samples);
assert(chunk_size <= len);
len -= chunk_size;
bool ok =
SDL_PutAudioStreamData(stream, ap->aout_buffer, chunk_size);
if (!ok) {
LOGW("Audio stream error: %s", SDL_GetError());
return;
}
}
}
static bool
@@ -61,23 +78,45 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
/ SC_TICK_FREQ;
assert(aout_samples <= 0xFFFF);
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels,
.samples = aout_samples,
.callback = sc_audio_player_sdl_callback,
.userdata = ap,
};
SDL_AudioSpec obtained;
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (!ap->device) {
LOGE("Could not open audio device: %s", SDL_GetError());
char str[5 + 1]; // max 65535
int r = snprintf(str, sizeof(str), "%" PRIu16, (uint16_t) aout_samples);
assert(r >= 0 && (size_t) r < sizeof(str));
(void) r;
if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_SAMPLE_FRAMES, str)) {
LOGE("Could not set audio output buffer");
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
// Make the buffer at least 1024 samples long (the hint is not always
// honored)
uint64_t aout_buffer_samples = MAX(1024, aout_samples);
ap->aout_buffer_size = aout_buffer_samples * sample_size;
ap->aout_buffer = malloc(ap->aout_buffer_size);
if (!ap->aout_buffer) {
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
SDL_AudioSpec spec = {
.freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels,
};
ap->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
&spec,
sc_audio_player_stream_callback, ap);
if (!ap->stream) {
LOGE("Could not open audio device: %s", SDL_GetError());
free(ap->aout_buffer);
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
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);
@@ -86,7 +125,14 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
(void) ok; // We don't care if it worked, at least we tried
}
SDL_PauseAudioDevice(ap->device, 0);
ok = SDL_ResumeAudioDevice(ap->device);
if (!ok) {
LOGE("Could not resume audio device: %s", SDL_GetError());
SDL_DestroyAudioStream(ap->stream);
free(ap->aout_buffer);
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
return true;
}
@@ -95,11 +141,16 @@ static void
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_audio_player *ap = DOWNCAST(sink);
assert(ap->stream);
assert(ap->device);
SDL_PauseAudioDevice(ap->device, 1);
SDL_CloseAudioDevice(ap->device);
SDL_PauseAudioDevice(ap->device);
// ap->device is owned by ap->stream
SDL_DestroyAudioStream(ap->stream);
sc_audio_regulator_destroy(&ap->audioreg);
free(ap->aout_buffer);
}
void

View File

@@ -3,7 +3,7 @@
#include "common.h"
#include <SDL2/SDL_audio.h>
#include <SDL3/SDL_audio.h>
#include "audio_regulator.h"
#include "trait/frame_sink.h"
@@ -22,7 +22,11 @@ struct sc_audio_player {
// SDL audio output buffer size
sc_tick output_buffer_duration;
SDL_AudioDeviceID device;
uint8_t *aout_buffer;
size_t aout_buffer_size;
SDL_AudioStream *stream;
SDL_AudioDeviceID device; // owned by the audio stream
struct sc_audio_regulator audioreg;
};

View File

@@ -6,7 +6,7 @@
#include <libavcodec/version.h>
#include <libavformat/version.h>
#include <libavutil/version.h>
#include <SDL2/SDL_version.h>
#include <SDL3/SDL_version.h>
#ifndef _WIN32
# define PRIu64_ PRIu64
@@ -61,20 +61,6 @@
# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
#endif
#if SDL_VERSION_ATLEAST(2, 0, 6)
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
#endif
#if SDL_VERSION_ATLEAST(2, 0, 8)
// <https://hg.libsdl.org/SDL/rev/dfde5d3f9781>
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
#endif
#if SDL_VERSION_ATLEAST(2, 0, 16)
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
#endif
#ifndef HAVE_STRDUP
char *strdup(const char *s);
#endif

View File

@@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
int16_t hscroll =
sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
int16_t vscroll =
sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
// Accept values in the range [-16, 16].
// Normalize to [-1, 1] in order to use sc_float_to_i16fp().
float hscroll_norm = msg->inject_scroll_event.hscroll / 16;
hscroll_norm = CLAMP(hscroll_norm, -1, 1);
float vscroll_norm = msg->inject_scroll_event.vscroll / 16;
vscroll_norm = CLAMP(vscroll_norm, -1, 1);
int16_t hscroll = sc_float_to_i16fp(hscroll_norm);
int16_t vscroll = sc_float_to_i16fp(vscroll_norm);
sc_write16be(&buf[13], (uint16_t) hscroll);
sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);

View File

@@ -53,7 +53,7 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
}
uint16_t id = sc_read16be(&buf[1]);
size_t size = sc_read16be(&buf[3]);
if (size < len - 5) {
if (size > len - 5) {
return 0; // not available
}
uint8_t *data = malloc(size);

View File

@@ -6,14 +6,18 @@
#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);
if (SDL_RenderSetLogicalSize(display->renderer,
icon_novideo->w, icon_novideo->h)) {
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
}
@@ -31,16 +35,13 @@ sc_display_init_novideo_icon(struct sc_display *display,
bool
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps) {
display->renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
display->renderer = SDL_CreateRenderer(window, NULL);
if (!display->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
return false;
}
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(display->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
const char *renderer_name = SDL_GetRendererName(display->renderer);
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
display->mipmaps = false;
@@ -56,8 +57,11 @@ sc_display_init(struct sc_display *display, SDL_Window *window,
#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.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
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);
@@ -94,14 +98,13 @@ sc_display_init(struct sc_display *display, SDL_Window *window,
display->texture = NULL;
display->pending.flags = 0;
display->pending.frame = NULL;
display->has_frame = false;
if (icon_novideo) {
// Without video, set a static scrcpy icon as window content
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
if (!ok) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
SDL_GL_DestroyContext(display->gl_context);
#endif
SDL_DestroyRenderer(display->renderer);
return false;
@@ -117,7 +120,7 @@ sc_display_destroy(struct sc_display *display) {
av_frame_free(&display->pending.frame);
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
SDL_GL_DestroyContext(display->gl_context);
#endif
if (display->texture) {
SDL_DestroyTexture(display->texture);
@@ -125,13 +128,63 @@ sc_display_destroy(struct sc_display *display) {
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) {
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_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
SDL_DestroyProperties(props);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
@@ -140,24 +193,49 @@ sc_display_create_texture(struct sc_display *display,
if (display->mipmaps) {
struct sc_opengl *gl = &display->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
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);
SDL_GL_UnbindTexture(texture);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return texture;
}
static inline void
sc_display_set_pending_size(struct sc_display *display, struct sc_size size) {
sc_display_set_pending_texture(struct sc_display *display,
struct sc_size size,
enum AVColorRange color_range) {
assert(!display->texture);
display->pending.size = size;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE;
display->pending.texture.size = size;
display->pending.texture.color_range = color_range;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_TEXTURE;
}
static bool
@@ -170,6 +248,7 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
}
}
av_frame_unref(display->pending.frame);
int r = av_frame_ref(display->pending.frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
@@ -181,22 +260,31 @@ sc_display_set_pending_frame(struct sc_display *display, const AVFrame *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_SIZE) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_TEXTURE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display, display->pending.size);
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_SIZE;
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(display, display->pending.frame);
bool ok = sc_display_update_texture_internal(display,
display->pending.frame);
if (!ok) {
return false;
}
@@ -209,15 +297,18 @@ sc_display_apply_pending(struct sc_display *display) {
}
static bool
sc_display_set_texture_size_internal(struct sc_display *display,
struct sc_size size) {
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);
display->texture =
sc_display_create_texture(display, size, color_space, color_range);
if (!display->texture) {
return false;
}
@@ -227,10 +318,13 @@ sc_display_set_texture_size_internal(struct sc_display *display,
}
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
bool ok = sc_display_set_texture_size_internal(display, size);
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_size(display, size);
sc_display_set_pending_texture(display, size, color_range);
return SC_DISPLAY_RESULT_PENDING;
}
@@ -238,38 +332,25 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
return SC_DISPLAY_RESULT_OK;
}
static SDL_YUV_CONVERSION_MODE
sc_display_to_sdl_color_range(enum AVColorRange color_range) {
return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG
: SDL_YUV_CONVERSION_AUTOMATIC;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
if (!display->has_frame) {
// First frame
display->has_frame = true;
// Configure YUV color range conversion
SDL_YUV_CONVERSION_MODE sdl_color_range =
sc_display_to_sdl_color_range(frame->color_range);
SDL_SetYUVConversionMode(sdl_color_range);
}
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
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 (ret) {
if (!ok) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (display->mipmaps) {
SDL_GL_BindTexture(display->texture, NULL, NULL);
display->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(display->texture);
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;
@@ -294,7 +375,7 @@ 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) {
SDL_RenderClear(display->renderer);
sc_sdl_render_clear(display->renderer);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
@@ -307,8 +388,10 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
SDL_Texture *texture = display->texture;
if (orientation == SC_ORIENTATION_0) {
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) {
SDL_FRect frect;
SDL_RectToFRect(geometry, &frect);
bool ok = SDL_RenderTexture(renderer, texture, NULL, &frect);
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
@@ -316,29 +399,27 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
const SDL_Rect *dstrect = NULL;
SDL_Rect rect;
SDL_FRect frect;
if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
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 {
dstrect = geometry;
SDL_RectToFRect(geometry, &frect);
}
SDL_RendererFlip flip = sc_orientation_is_mirror(orientation)
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
NULL, flip);
if (ret) {
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;
}
}
SDL_RenderPresent(display->renderer);
sc_sdl_render_present(display->renderer);
return SC_DISPLAY_RESULT_OK;
}

View File

@@ -6,7 +6,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <libavutil/frame.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "coords.h"
#include "opengl.h"
@@ -22,20 +22,23 @@ struct sc_display {
struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext *gl_context;
SDL_GLContext gl_context;
#endif
bool mipmaps;
uint32_t texture_id; // only set if mipmaps is enabled
struct {
#define SC_DISPLAY_PENDING_FLAG_SIZE 1
#define SC_DISPLAY_PENDING_FLAG_TEXTURE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct sc_size size;
struct {
struct sc_size size;
enum AVColorSpace color_space;
enum AVColorRange color_range;
} texture;
AVFrame *frame;
} pending;
bool has_frame;
};
enum sc_display_result {
@@ -52,7 +55,9 @@ void
sc_display_destroy(struct sc_display *display);
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
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);

View File

@@ -9,11 +9,8 @@ bool
sc_push_event_impl(uint32_t type, const char *name) {
SDL_Event event;
event.type = type;
int ret = SDL_PushEvent(&event);
// ret < 0: error (queue full)
// ret == 0: event was filtered
// ret == 1: success
if (ret != 1) {
bool ok = SDL_PushEvent(&event);
if (!ok) {
LOGE("Could not post %s event: %s", name, SDL_GetError());
return false;
}
@@ -30,34 +27,25 @@ sc_post_to_main_thread(sc_runnable_fn run, void *userdata) {
.data2 = userdata,
},
};
int ret = SDL_PushEvent(&event);
// ret < 0: error (queue full)
// ret == 0: event was filtered
// ret == 1: success
if (ret != 1) {
if (ret == 0) {
// if ret == 0, this is expected on exit, log in debug mode
LOGD("Could not post runnable to main thread (filtered)");
} else {
assert(ret < 0);
LOGW("Could not post runnable to main thread: %s", SDL_GetError());
}
bool ok = SDL_PushEvent(&event);
if (!ok) {
LOGW("Could not post runnable to main thread: %s", SDL_GetError());
return false;
}
return true;
}
static int SDLCALL
static bool SDLCALL
task_event_filter(void *userdata, SDL_Event *event) {
(void) userdata;
if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Reject this event type from now on
return 0;
return false;
}
return 1;
return true;
}
void

View File

@@ -5,10 +5,10 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_events.h>
#include <SDL3/SDL_events.h>
enum {
SC_EVENT_NEW_FRAME = SDL_USEREVENT,
SC_EVENT_NEW_FRAME = SDL_EVENT_USER,
SC_EVENT_RUN_ON_MAIN_THREAD,
SC_EVENT_DEVICE_DISCONNECTED,
SC_EVENT_SERVER_CONNECTION_FAILED,
@@ -16,7 +16,6 @@ enum {
SC_EVENT_USB_DEVICE_DISCONNECTED,
SC_EVENT_DEMUXER_ERROR,
SC_EVENT_RECORDER_ERROR,
SC_EVENT_SCREEN_INIT_SIZE,
SC_EVENT_TIME_LIMIT_REACHED,
SC_EVENT_CONTROLLER_ERROR,
SC_EVENT_AOA_OPEN_ERROR,

View File

@@ -9,7 +9,7 @@
#include "hid/hid_event.h"
#include "input_events.h"
// See "SDL2/SDL_scancode.h".
// See "SDL3/SDL_scancode.h".
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
// HID protocol.
// 0x65 is Application, typically AT-101 Keyboard ends here.

View File

@@ -3,8 +3,8 @@
#include <stdint.h>
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
// 1 byte for wheel motion
#define SC_HID_MOUSE_INPUT_SIZE 4
// 1 byte for wheel motion, 1 byte for hozizontal scrolling
#define SC_HID_MOUSE_INPUT_SIZE 5
/**
* Mouse descriptor from the specification:
@@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06,
// Usage Page (Consumer Page)
0x05, 0x0C,
// Usage(AC Pan)
0x0A, 0x38, 0x02,
// Logical Minimum (-127)
0x15, 0x81,
// Logical Maximum (127)
0x25, 0x7F,
// Report Size (8)
0x75, 0x08,
// Report Count (1)
0x95, 0x01,
// Input (Data, Variable, Relative): 1 byte (AC Pan)
0x81, 0x06,
// End Collection
0xC0,
@@ -151,6 +166,12 @@ sc_hid_buttons_from_buttons_state(uint8_t buttons_state) {
return c;
}
void
sc_hid_mouse_init(struct sc_hid_mouse *hid) {
hid->residual_hscroll = 0;
hid->residual_vscroll = 0;
}
void
sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
const struct sc_mouse_motion_event *event) {
@@ -160,7 +181,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = CLAMP(event->xrel, -127, 127);
data[2] = CLAMP(event->yrel, -127, 127);
data[3] = 0; // wheel coordinates only used for scrolling
data[3] = 0; // no vertical scrolling
data[4] = 0; // no horizontal scrolling
}
void
@@ -172,22 +194,42 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion
data[2] = 0; // no y motion
data[3] = 0; // wheel coordinates only used for scrolling
data[3] = 0; // no vertical scrolling
data[4] = 0; // no horizontal scrolling
}
void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
static int8_t
consume_scroll_integer(float *scroll) {
float value = CLAMP(*scroll, -127, 127);
int8_t consume = value; // truncate towards 0
float residual = value - consume;
*scroll = residual;
return consume;
}
bool
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_mouse *hid,
struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event) {
sc_hid_mouse_input_init(hid_input);
hid->residual_hscroll += event->hscroll;
hid->residual_vscroll += event->vscroll;
int8_t hscroll = consume_scroll_integer(&hid->residual_hscroll);
int8_t vscroll = consume_scroll_integer(&hid->residual_vscroll);
if (!hscroll && !vscroll) {
// Not enough scrolling to inject a scroll event
return false;
}
uint8_t *data = hid_input->data;
data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion
data[2] = 0; // no y motion
// In practice, vscroll is always -1, 0 or 1, but in theory other values
// are possible
data[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
data[3] = vscroll;
data[4] = hscroll;
return true;
}
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {

View File

@@ -8,6 +8,13 @@
#define SC_HID_ID_MOUSE 2
struct sc_hid_mouse {
float residual_hscroll;
float residual_vscroll;
};
void sc_hid_mouse_init(struct sc_hid_mouse *hid);
void
sc_hid_mouse_generate_open(struct sc_hid_open *hid_open);
@@ -22,8 +29,9 @@ void
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event);
void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
bool
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_mouse *hid,
struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event);
#endif

View File

@@ -10,7 +10,7 @@
#include <libavutil/avutil.h>
#include <libavutil/pixdesc.h>
#include <libavutil/pixfmt.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "config.h"
#include "util/env.h"
@@ -156,13 +156,7 @@ free_ctx:
return result;
}
#if !SDL_VERSION_ATLEAST(2, 0, 10)
// SDL_PixelFormatEnum has been introduced in SDL 2.0.10. Use int for older SDL
// versions.
typedef int SDL_PixelFormatEnum;
#endif
static SDL_PixelFormatEnum
static SDL_PixelFormat
to_sdl_pixel_format(enum AVPixelFormat fmt) {
switch (fmt) {
case AV_PIX_FMT_RGB24: return SDL_PIXELFORMAT_RGB24;
@@ -172,13 +166,11 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) {
case AV_PIX_FMT_ABGR: return SDL_PIXELFORMAT_ABGR32;
case AV_PIX_FMT_BGRA: return SDL_PIXELFORMAT_BGRA32;
case AV_PIX_FMT_RGB565BE: return SDL_PIXELFORMAT_RGB565;
case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_RGB555;
case AV_PIX_FMT_RGB555BE: return SDL_PIXELFORMAT_XRGB1555;
case AV_PIX_FMT_BGR565BE: return SDL_PIXELFORMAT_BGR565;
case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_BGR555;
case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_RGB444;
#if SDL_VERSION_ATLEAST(2, 0, 12)
case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_BGR444;
#endif
case AV_PIX_FMT_BGR555BE: return SDL_PIXELFORMAT_XBGR1555;
case AV_PIX_FMT_RGB444BE: return SDL_PIXELFORMAT_XRGB4444;
case AV_PIX_FMT_BGR444BE: return SDL_PIXELFORMAT_XBGR4444;
case AV_PIX_FMT_PAL8: return SDL_PIXELFORMAT_INDEX8;
default: return SDL_PIXELFORMAT_UNKNOWN;
}
@@ -203,20 +195,16 @@ load_from_path(const char *path) {
goto error;
}
SDL_PixelFormatEnum format = to_sdl_pixel_format(frame->format);
SDL_PixelFormat format = to_sdl_pixel_format(frame->format);
if (format == SDL_PIXELFORMAT_UNKNOWN) {
LOGE("Unsupported icon pixel format: %s (%d)", desc->name,
frame->format);
goto error;
}
int bits_per_pixel = av_get_bits_per_pixel(desc);
SDL_Surface *surface =
SDL_CreateRGBSurfaceWithFormatFrom(frame->data[0],
frame->width, frame->height,
bits_per_pixel,
frame->linesize[0],
format);
SDL_CreateSurfaceFrom(frame->width, frame->height, format,
frame->data[0], frame->linesize[0]);
if (!surface) {
LOGE("Could not create icon surface");
@@ -248,17 +236,35 @@ load_from_path(const char *path) {
#endif
}
SDL_Palette *palette = surface->format->palette;
assert(palette);
int ret = SDL_SetPaletteColors(palette, colors, 0, 256);
if (ret) {
SDL_Palette *palette = SDL_CreateSurfacePalette(surface);
if (!palette) {
LOGE("Could not create palette");
SDL_DestroySurface(surface);
goto error;
}
bool ok = SDL_SetPaletteColors(palette, colors, 0, 256);
if (!ok) {
LOGE("Could not set palette colors");
SDL_FreeSurface(surface);
SDL_DestroySurface(surface);
goto error;
}
}
surface->userdata = frame; // frame owns the data
SDL_PropertiesID props = SDL_GetSurfaceProperties(surface);
if (!props) {
LOGE("Could not get surface properties: %s", SDL_GetError());
SDL_DestroySurface(surface);
goto error;
}
// frame owns the data
bool ok = SDL_SetPointerProperty(props, "sc_frame", frame);
if (!ok) {
LOGE("Could not set pointer property: %s", SDL_GetError());
SDL_DestroySurface(surface);
goto error;
}
return surface;
@@ -281,8 +287,10 @@ scrcpy_icon_load(void) {
void
scrcpy_icon_destroy(SDL_Surface *icon) {
AVFrame *frame = icon->userdata;
SDL_PropertiesID props = SDL_GetSurfaceProperties(icon);
assert(props);
AVFrame *frame = SDL_GetPointerProperty(props, "sc_frame", NULL);
assert(frame);
av_frame_free(&frame);
SDL_FreeSurface(icon);
SDL_DestroySurface(icon);
}

View File

@@ -3,7 +3,7 @@
#include "common.h"
#include <SDL2/SDL_surface.h>
#include <SDL3/SDL_surface.h>
SDL_Surface *
scrcpy_icon_load(void);

View File

@@ -6,7 +6,7 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_events.h>
#include <SDL3/SDL_events.h>
#include "coords.h"
@@ -14,7 +14,8 @@
* for simplicity.
*
* This scrcpy input events API is designed to be consumed by input event
* processors (sc_key_processor and sc_mouse_processor, see app/src/trait/).
* processors (sc_key_processor, sc_mouse_processor and sc_gamepad_processor,
* see app/src/trait/).
*
* One major semantic difference between SDL input events and scrcpy input
* events is their frame of reference (for mouse and touch events): SDL events
@@ -43,17 +44,17 @@
*/
enum sc_mod {
SC_MOD_LSHIFT = KMOD_LSHIFT,
SC_MOD_RSHIFT = KMOD_RSHIFT,
SC_MOD_LCTRL = KMOD_LCTRL,
SC_MOD_RCTRL = KMOD_RCTRL,
SC_MOD_LALT = KMOD_LALT,
SC_MOD_RALT = KMOD_RALT,
SC_MOD_LGUI = KMOD_LGUI,
SC_MOD_RGUI = KMOD_RGUI,
SC_MOD_LSHIFT = SDL_KMOD_LSHIFT,
SC_MOD_RSHIFT = SDL_KMOD_RSHIFT,
SC_MOD_LCTRL = SDL_KMOD_LCTRL,
SC_MOD_RCTRL = SDL_KMOD_RCTRL,
SC_MOD_LALT = SDL_KMOD_LALT,
SC_MOD_RALT = SDL_KMOD_RALT,
SC_MOD_LGUI = SDL_KMOD_LGUI,
SC_MOD_RGUI = SDL_KMOD_RGUI,
SC_MOD_NUM = KMOD_NUM,
SC_MOD_CAPS = KMOD_CAPS,
SC_MOD_NUM = SDL_KMOD_NUM,
SC_MOD_CAPS = SDL_KMOD_CAPS,
};
enum sc_action {
@@ -70,12 +71,12 @@ enum sc_keycode {
SC_KEYCODE_TAB = SDLK_TAB,
SC_KEYCODE_SPACE = SDLK_SPACE,
SC_KEYCODE_EXCLAIM = SDLK_EXCLAIM,
SC_KEYCODE_QUOTEDBL = SDLK_QUOTEDBL,
SC_KEYCODE_QUOTEDBL = SDLK_DBLAPOSTROPHE,
SC_KEYCODE_HASH = SDLK_HASH,
SC_KEYCODE_PERCENT = SDLK_PERCENT,
SC_KEYCODE_DOLLAR = SDLK_DOLLAR,
SC_KEYCODE_AMPERSAND = SDLK_AMPERSAND,
SC_KEYCODE_QUOTE = SDLK_QUOTE,
SC_KEYCODE_QUOTE = SDLK_APOSTROPHE,
SC_KEYCODE_LEFTPAREN = SDLK_LEFTPAREN,
SC_KEYCODE_RIGHTPAREN = SDLK_RIGHTPAREN,
SC_KEYCODE_ASTERISK = SDLK_ASTERISK,
@@ -107,33 +108,33 @@ enum sc_keycode {
SC_KEYCODE_RIGHTBRACKET = SDLK_RIGHTBRACKET,
SC_KEYCODE_CARET = SDLK_CARET,
SC_KEYCODE_UNDERSCORE = SDLK_UNDERSCORE,
SC_KEYCODE_BACKQUOTE = SDLK_BACKQUOTE,
SC_KEYCODE_a = SDLK_a,
SC_KEYCODE_b = SDLK_b,
SC_KEYCODE_c = SDLK_c,
SC_KEYCODE_d = SDLK_d,
SC_KEYCODE_e = SDLK_e,
SC_KEYCODE_f = SDLK_f,
SC_KEYCODE_g = SDLK_g,
SC_KEYCODE_h = SDLK_h,
SC_KEYCODE_i = SDLK_i,
SC_KEYCODE_j = SDLK_j,
SC_KEYCODE_k = SDLK_k,
SC_KEYCODE_l = SDLK_l,
SC_KEYCODE_m = SDLK_m,
SC_KEYCODE_n = SDLK_n,
SC_KEYCODE_o = SDLK_o,
SC_KEYCODE_p = SDLK_p,
SC_KEYCODE_q = SDLK_q,
SC_KEYCODE_r = SDLK_r,
SC_KEYCODE_s = SDLK_s,
SC_KEYCODE_t = SDLK_t,
SC_KEYCODE_u = SDLK_u,
SC_KEYCODE_v = SDLK_v,
SC_KEYCODE_w = SDLK_w,
SC_KEYCODE_x = SDLK_x,
SC_KEYCODE_y = SDLK_y,
SC_KEYCODE_z = SDLK_z,
SC_KEYCODE_BACKQUOTE = SDLK_GRAVE,
SC_KEYCODE_a = SDLK_A,
SC_KEYCODE_b = SDLK_B,
SC_KEYCODE_c = SDLK_C,
SC_KEYCODE_d = SDLK_D,
SC_KEYCODE_e = SDLK_E,
SC_KEYCODE_f = SDLK_F,
SC_KEYCODE_g = SDLK_G,
SC_KEYCODE_h = SDLK_H,
SC_KEYCODE_i = SDLK_I,
SC_KEYCODE_j = SDLK_J,
SC_KEYCODE_k = SDLK_K,
SC_KEYCODE_l = SDLK_L,
SC_KEYCODE_m = SDLK_M,
SC_KEYCODE_n = SDLK_N,
SC_KEYCODE_o = SDLK_O,
SC_KEYCODE_p = SDLK_P,
SC_KEYCODE_q = SDLK_Q,
SC_KEYCODE_r = SDLK_R,
SC_KEYCODE_s = SDLK_S,
SC_KEYCODE_t = SDLK_T,
SC_KEYCODE_u = SDLK_U,
SC_KEYCODE_v = SDLK_V,
SC_KEYCODE_w = SDLK_W,
SC_KEYCODE_x = SDLK_X,
SC_KEYCODE_y = SDLK_Y,
SC_KEYCODE_z = SDLK_Z,
SC_KEYCODE_CAPSLOCK = SDLK_CAPSLOCK,
@@ -315,43 +316,40 @@ enum sc_scancode {
// to avoid unnecessary conversions (and confusion).
enum sc_mouse_button {
SC_MOUSE_BUTTON_UNKNOWN = 0,
SC_MOUSE_BUTTON_LEFT = SDL_BUTTON(SDL_BUTTON_LEFT),
SC_MOUSE_BUTTON_RIGHT = SDL_BUTTON(SDL_BUTTON_RIGHT),
SC_MOUSE_BUTTON_MIDDLE = SDL_BUTTON(SDL_BUTTON_MIDDLE),
SC_MOUSE_BUTTON_X1 = SDL_BUTTON(SDL_BUTTON_X1),
SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2),
SC_MOUSE_BUTTON_LEFT = SDL_BUTTON_MASK(SDL_BUTTON_LEFT),
SC_MOUSE_BUTTON_RIGHT = SDL_BUTTON_MASK(SDL_BUTTON_RIGHT),
SC_MOUSE_BUTTON_MIDDLE = SDL_BUTTON_MASK(SDL_BUTTON_MIDDLE),
SC_MOUSE_BUTTON_X1 = SDL_BUTTON_MASK(SDL_BUTTON_X1),
SC_MOUSE_BUTTON_X2 = SDL_BUTTON_MASK(SDL_BUTTON_X2),
};
// Use the naming from SDL3 for gamepad axis and buttons:
// <https://wiki.libsdl.org/SDL3/README/migration>
enum sc_gamepad_axis {
SC_GAMEPAD_AXIS_UNKNOWN = -1,
SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX,
SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY,
SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX,
SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY,
SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT,
SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
SC_GAMEPAD_AXIS_LEFTX = SDL_GAMEPAD_AXIS_LEFTX,
SC_GAMEPAD_AXIS_LEFTY = SDL_GAMEPAD_AXIS_LEFTY,
SC_GAMEPAD_AXIS_RIGHTX = SDL_GAMEPAD_AXIS_RIGHTX,
SC_GAMEPAD_AXIS_RIGHTY = SDL_GAMEPAD_AXIS_RIGHTY,
SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_GAMEPAD_AXIS_LEFT_TRIGGER,
SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_GAMEPAD_AXIS_RIGHT_TRIGGER,
};
enum sc_gamepad_button {
SC_GAMEPAD_BUTTON_UNKNOWN = -1,
SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A,
SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B,
SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X,
SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y,
SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK,
SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE,
SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START,
SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK,
SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK,
SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP,
SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT,
SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
SC_GAMEPAD_BUTTON_SOUTH = SDL_GAMEPAD_BUTTON_SOUTH,
SC_GAMEPAD_BUTTON_EAST = SDL_GAMEPAD_BUTTON_EAST,
SC_GAMEPAD_BUTTON_WEST = SDL_GAMEPAD_BUTTON_WEST,
SC_GAMEPAD_BUTTON_NORTH = SDL_GAMEPAD_BUTTON_NORTH,
SC_GAMEPAD_BUTTON_BACK = SDL_GAMEPAD_BUTTON_BACK,
SC_GAMEPAD_BUTTON_GUIDE = SDL_GAMEPAD_BUTTON_GUIDE,
SC_GAMEPAD_BUTTON_START = SDL_GAMEPAD_BUTTON_START,
SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_GAMEPAD_BUTTON_LEFT_STICK,
SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_GAMEPAD_BUTTON_RIGHT_STICK,
SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_GAMEPAD_BUTTON_LEFT_SHOULDER,
SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER,
SC_GAMEPAD_BUTTON_DPAD_UP = SDL_GAMEPAD_BUTTON_DPAD_UP,
SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_GAMEPAD_BUTTON_DPAD_DOWN,
SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_GAMEPAD_BUTTON_DPAD_LEFT,
SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_GAMEPAD_BUTTON_DPAD_RIGHT,
};
static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod),
@@ -411,10 +409,9 @@ struct sc_touch_event {
float pressure;
};
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
// The ID value starts at 0 and increments from there. The value -1 is an
// invalid ID.
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
// As documented in <https://wiki.libsdl.org/SDL3/SDL_JoystickID>:
// The value 0 is an invalid ID.
#define SC_GAMEPAD_ID_INVALID 0
struct sc_gamepad_device_event {
uint32_t gamepad_id;
@@ -449,8 +446,8 @@ sc_scancode_from_sdl(SDL_Scancode scancode) {
static inline enum sc_action
sc_action_from_sdl_keyboard_type(uint32_t type) {
assert(type == SDL_KEYDOWN || type == SDL_KEYUP);
if (type == SDL_KEYDOWN) {
assert(type == SDL_EVENT_KEY_DOWN || type == SDL_EVENT_KEY_UP);
if (type == SDL_EVENT_KEY_DOWN) {
return SC_ACTION_DOWN;
}
return SC_ACTION_UP;
@@ -458,8 +455,8 @@ sc_action_from_sdl_keyboard_type(uint32_t type) {
static inline enum sc_action
sc_action_from_sdl_mousebutton_type(uint32_t type) {
assert(type == SDL_MOUSEBUTTONDOWN || type == SDL_MOUSEBUTTONUP);
if (type == SDL_MOUSEBUTTONDOWN) {
assert(type == SDL_EVENT_MOUSE_BUTTON_DOWN || type == SDL_EVENT_MOUSE_BUTTON_UP);
if (type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
return SC_ACTION_DOWN;
}
return SC_ACTION_UP;
@@ -467,12 +464,12 @@ sc_action_from_sdl_mousebutton_type(uint32_t type) {
static inline enum sc_touch_action
sc_touch_action_from_sdl(uint32_t type) {
assert(type == SDL_FINGERMOTION || type == SDL_FINGERDOWN ||
type == SDL_FINGERUP);
if (type == SDL_FINGERMOTION) {
assert(type == SDL_EVENT_FINGER_MOTION || type == SDL_EVENT_FINGER_DOWN ||
type == SDL_EVENT_FINGER_UP);
if (type == SDL_EVENT_FINGER_MOTION) {
return SC_TOUCH_ACTION_MOVE;
}
if (type == SDL_FINGERDOWN) {
if (type == SDL_EVENT_FINGER_DOWN) {
return SC_TOUCH_ACTION_DOWN;
}
return SC_TOUCH_ACTION_UP;
@@ -482,7 +479,7 @@ static inline enum sc_mouse_button
sc_mouse_button_from_sdl(uint8_t button) {
if (button >= SDL_BUTTON_LEFT && button <= SDL_BUTTON_X2) {
// SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index)
return SDL_BUTTON(button);
return SDL_BUTTON_MASK(button);
}
return SC_MOUSE_BUTTON_UNKNOWN;
@@ -498,9 +495,9 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
static inline enum sc_gamepad_axis
sc_gamepad_axis_from_sdl(uint8_t axis) {
if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
if (axis <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
// SC_GAMEPAD_AXIS_* constants are initialized from
// SDL_CONTROLLER_AXIS_*
// SDL_GAMEPAD_AXIS_*
return axis;
}
return SC_GAMEPAD_AXIS_UNKNOWN;
@@ -508,18 +505,18 @@ sc_gamepad_axis_from_sdl(uint8_t axis) {
static inline enum sc_gamepad_button
sc_gamepad_button_from_sdl(uint8_t button) {
if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) {
if (button <= SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
// SC_GAMEPAD_BUTTON_* constants are initialized from
// SDL_CONTROLLER_BUTTON_*
// SDL_GAMEPAD_BUTTON_*
return button;
}
return SC_GAMEPAD_BUTTON_UNKNOWN;
}
static inline enum sc_action
sc_action_from_sdl_controllerbutton_type(uint32_t type) {
assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP);
if (type == SDL_CONTROLLERBUTTONDOWN) {
sc_action_from_sdl_gamepad_button_type(uint32_t type) {
assert(type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || type == SDL_EVENT_GAMEPAD_BUTTON_UP);
if (type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
return SC_ACTION_DOWN;
}
return SC_ACTION_UP;

View File

@@ -3,7 +3,7 @@
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "android/input.h"
#include "android/keycodes.h"
@@ -11,6 +11,7 @@
#include "screen.h"
#include "shortcut_mod.h"
#include "util/log.h"
#include "util/sdl.h"
void
sc_input_manager_init(struct sc_input_manager *im,
@@ -374,11 +375,11 @@ sc_input_manager_process_key(struct sc_input_manager *im,
bool paused = im->screen->paused;
bool video = im->screen->video;
SDL_Keycode sdl_keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
SDL_Keycode sdl_keycode = event->key;
uint16_t mod = event->mod;
bool down = event->type == SDL_EVENT_KEY_DOWN;
bool ctrl = event->mod & SDL_KMOD_CTRL;
bool shift = event->mod & SDL_KMOD_SHIFT;
bool repeat = event->repeat;
// Either the modifier includes a shortcut modifier, or the key
@@ -402,39 +403,39 @@ 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:
case SDLK_H:
if (im->kp && !shift && !repeat && !paused) {
action_home(im, action);
}
return;
case SDLK_b: // fall-through
case SDLK_B: // fall-through
case SDLK_BACKSPACE:
if (im->kp && !shift && !repeat && !paused) {
action_back(im, action);
}
return;
case SDLK_s:
case SDLK_S:
if (im->kp && !shift && !repeat && !paused) {
action_app_switch(im, action);
}
return;
case SDLK_m:
case SDLK_M:
if (im->kp && !shift && !repeat && !paused) {
action_menu(im, action);
}
return;
case SDLK_p:
case SDLK_P:
if (im->kp && !shift && !repeat && !paused) {
action_power(im, action);
}
return;
case SDLK_o:
case SDLK_O:
if (control && !repeat && down && !paused) {
bool on = shift;
set_display_power(im, on);
}
return;
case SDLK_z:
case SDLK_Z:
if (video && down && !repeat) {
sc_screen_set_paused(im->screen, !shift);
}
@@ -483,17 +484,17 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
return;
case SDLK_c:
case SDLK_C:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_COPY);
}
return;
case SDLK_x:
case SDLK_X:
if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(im, SC_COPY_KEY_CUT);
}
return;
case SDLK_v:
case SDLK_V:
if (im->kp && !repeat && down && !paused) {
if (shift || im->legacy_paste) {
// inject the text as input events
@@ -505,27 +506,27 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
return;
case SDLK_f:
case SDLK_F:
if (video && !shift && !repeat && down) {
sc_screen_toggle_fullscreen(im->screen);
}
return;
case SDLK_w:
case SDLK_W:
if (video && !shift && !repeat && down) {
sc_screen_resize_to_fit(im->screen);
}
return;
case SDLK_g:
case SDLK_G:
if (video && !shift && !repeat && down) {
sc_screen_resize_to_pixel_perfect(im->screen);
}
return;
case SDLK_i:
case SDLK_I:
if (video && !shift && !repeat && down) {
switch_fps_counter_state(im);
}
return;
case SDLK_n:
case SDLK_N:
if (control && !repeat && down && !paused) {
if (shift) {
collapse_panels(im);
@@ -536,7 +537,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
return;
case SDLK_r:
case SDLK_R:
if (control && !repeat && down && !paused) {
if (shift) {
reset_video(im);
@@ -545,7 +546,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
return;
case SDLK_k:
case SDLK_K:
if (control && !shift && !repeat && down && !paused
&& im->kp && im->kp->hid) {
// Only if the current keyboard is hid
@@ -562,7 +563,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat;
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_V && down && !repeat;
if (im->clipboard_autosync && is_ctrl_v) {
if (im->legacy_paste) {
// inject the text as input events
@@ -595,7 +596,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
}
enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode);
enum sc_scancode scancode = sc_scancode_from_sdl(event->scancode);
if (scancode == SC_SCANCODE_UNKNOWN) {
return;
}
@@ -605,7 +606,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
.keycode = keycode,
.scancode = scancode,
.repeat = event->repeat,
.mods_state = sc_mods_state_from_sdl(event->keysym.mod),
.mods_state = sc_mods_state_from_sdl(event->mod),
};
assert(im->kp->ops->process_key);
@@ -672,13 +673,12 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
return;
}
int dw;
int dh;
SDL_GL_GetDrawableSize(im->screen->window, &dw, &dh);
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(im->screen->window);
// SDL touch event coordinates are normalized in the range [0; 1]
int32_t x = event->x * dw;
int32_t y = event->y * dh;
int32_t x = event->x * (int32_t) drawable_size.width;
int32_t y = event->y * (int32_t) drawable_size.height;
struct sc_touch_event evt = {
.position = {
@@ -687,7 +687,7 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
sc_screen_convert_drawable_to_frame_coords(im->screen, x, y),
},
.action = sc_touch_action_from_sdl(event->type),
.pointer_id = event->fingerId,
.pointer_id = event->fingerID,
.pressure = event->pressure,
};
@@ -723,7 +723,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
bool down = event->type == SDL_MOUSEBUTTONDOWN;
bool down = event->type == SDL_EVENT_MOUSE_BUTTON_DOWN;
enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button);
if (button == SC_MOUSE_BUTTON_UNKNOWN) {
@@ -736,8 +736,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
}
SDL_Keymod keymod = SDL_GetModState();
bool ctrl_pressed = keymod & KMOD_CTRL;
bool shift_pressed = keymod & KMOD_SHIFT;
bool ctrl_pressed = keymod & SDL_KMOD_CTRL;
bool shift_pressed = keymod & SDL_KMOD_SHIFT;
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
@@ -889,20 +889,15 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
}
// mouse_x and mouse_y are expressed in pixels relative to the window
int mouse_x;
int mouse_y;
float mouse_x;
float mouse_y;
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
(void) buttons; // Actual buttons are tracked manually to ignore shortcuts
struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
#if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
#else
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.hscroll = event->x,
.vscroll = event->y,
.buttons_state = im->mouse_buttons_state,
};
@@ -911,31 +906,31 @@ 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_ControllerDeviceEvent *event) {
if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) {
LOGW("Could not open game controller");
const SDL_GamepadDeviceEvent *event) {
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
return;
}
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc);
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (!joystick) {
LOGW("Could not get controller joystick");
SDL_GameControllerClose(gc);
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
return;
}
struct sc_gamepad_device_event evt = {
.gamepad_id = SDL_JoystickInstanceID(joystick),
.gamepad_id = SDL_GetJoystickID(joystick),
};
im->gp->ops->process_gamepad_added(im->gp, &evt);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) {
SDL_GameControllerClose(gc);
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
} else {
LOGW("Unknown gamepad device removed");
}
@@ -952,7 +947,7 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
static void
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
const SDL_ControllerAxisEvent *event) {
const SDL_GamepadAxisEvent *event) {
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
@@ -968,7 +963,7 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
static void
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
const SDL_ControllerButtonEvent *event) {
const SDL_GamepadButtonEvent *event) {
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
@@ -976,7 +971,7 @@ sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.button = button,
};
im->gp->ops->process_gamepad_button(im->gp, &evt);
@@ -991,8 +986,8 @@ is_apk(const char *file) {
static void
sc_input_manager_process_file(struct sc_input_manager *im,
const SDL_DropEvent *event) {
char *file = strdup(event->file);
SDL_free(event->file);
assert(event->type == SDL_EVENT_DROP_FILE);
char *file = strdup(event->data);
if (!file) {
LOG_OOM();
return;
@@ -1016,66 +1011,66 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
switch (event->type) {
case SDL_TEXTINPUT:
case SDL_EVENT_TEXT_INPUT:
if (!im->kp || paused) {
break;
}
sc_input_manager_process_text_input(im, &event->text);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
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_MOUSEMOTION:
case SDL_EVENT_MOUSE_MOTION:
if (!im->mp || paused) {
break;
}
sc_input_manager_process_mouse_motion(im, &event->motion);
break;
case SDL_MOUSEWHEEL:
case SDL_EVENT_MOUSE_WHEEL:
if (!im->mp || paused) {
break;
}
sc_input_manager_process_mouse_wheel(im, &event->wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
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_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
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_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
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->cdevice);
sc_input_manager_process_gamepad_device(im, &event->gdevice);
break;
case SDL_CONTROLLERAXISMOTION:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
if (!im->gp || paused) {
break;
}
sc_input_manager_process_gamepad_axis(im, &event->caxis);
sc_input_manager_process_gamepad_axis(im, &event->gaxis);
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
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->cbutton);
sc_input_manager_process_gamepad_button(im, &event->gbutton);
break;
case SDL_DROPFILE: {
case SDL_EVENT_DROP_FILE: {
if (!control) {
break;
}

View File

@@ -5,8 +5,8 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_keycode.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_keycode.h>
#include "controller.h"
#include "file_pusher.h"

View File

@@ -1,16 +1,18 @@
#include "common.h"
#include <stdbool.h>
#include <stdio.h>
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "cli.h"
#include "options.h"
#include "scrcpy.h"
#include "usb/scrcpy_otg.h"
#ifdef HAVE_USB
# include "usb/scrcpy_otg.h"
#endif
#include "util/log.h"
#include "util/net.h"
#include "util/thread.h"

View File

@@ -20,14 +20,11 @@ bool
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
const SDL_Event *event) {
switch (event->type) {
case SDL_WINDOWEVENT:
if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
sc_mouse_capture_set_active(mc, false);
return true;
}
break;
case SDL_KEYDOWN: {
SDL_Keycode key = event->key.keysym.sym;
case SDL_EVENT_WINDOW_FOCUS_LOST:
sc_mouse_capture_set_active(mc, false);
return true;
case SDL_EVENT_KEY_DOWN: {
SDL_Keycode key = event->key.key;
if (sc_mouse_capture_is_capture_key(mc, key)) {
if (!mc->mouse_capture_key_pressed) {
mc->mouse_capture_key_pressed = key;
@@ -41,8 +38,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
}
break;
}
case SDL_KEYUP: {
SDL_Keycode key = event->key.keysym.sym;
case SDL_EVENT_KEY_UP: {
SDL_Keycode key = event->key.key;
SDL_Keycode cap = mc->mouse_capture_key_pressed;
mc->mouse_capture_key_pressed = 0;
if (sc_mouse_capture_is_capture_key(mc, key)) {
@@ -56,24 +53,24 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
}
break;
}
case SDL_MOUSEWHEEL:
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (!sc_mouse_capture_is_active(mc)) {
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
// the event
// The mouse will be captured on SDL_EVENT_MOUSE_BUTTON_UP, so
// consume the event
return true;
}
break;
case SDL_MOUSEBUTTONUP:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (!sc_mouse_capture_is_active(mc)) {
sc_mouse_capture_set_active(mc, true);
return true;
}
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
case SDL_EVENT_FINGER_MOTION:
case SDL_EVENT_FINGER_DOWN:
case SDL_EVENT_FINGER_UP:
// Touch events are not compatible with relative mode
// (coordinates are not relative), so consume the event
return true;
@@ -84,27 +81,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
void
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(mc->window, &x, &y);
SDL_GetWindowSize(mc->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(mc->window, w / 2, h / 2);
}
}
#else
(void) mc;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
bool ok = SDL_SetWindowRelativeMouseMode(mc->window, capture);
if (!ok) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
@@ -112,8 +90,7 @@ sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
bool
sc_mouse_capture_is_active(struct sc_mouse_capture *mc) {
(void) mc;
return SDL_GetRelativeMouseMode();
return SDL_GetWindowRelativeMouseMode(mc->window);
}
void

View File

@@ -5,7 +5,7 @@
#include <stdbool.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
struct sc_mouse_capture {
SDL_Window *window;

View File

@@ -3,21 +3,29 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
void
sc_opengl_init(struct sc_opengl *gl) {
gl->GetString = SDL_GL_GetProcAddress("glGetString");
gl->GetString = (const GLubyte *(*)(GLenum))
SDL_GL_GetProcAddress("glGetString");
assert(gl->GetString);
gl->TexParameterf = SDL_GL_GetProcAddress("glTexParameterf");
gl->BindTexture = (void (*)(GLenum, GLuint))
SDL_GL_GetProcAddress("glBindTexture");
assert(gl->BindTexture);
gl->TexParameterf = (void (*)(GLenum, GLenum, GLfloat))
SDL_GL_GetProcAddress("glTexParameterf");
assert(gl->TexParameterf);
gl->TexParameteri = SDL_GL_GetProcAddress("glTexParameteri");
gl->TexParameteri = (void (*)(GLenum, GLenum, GLint))
SDL_GL_GetProcAddress("glTexParameteri");
assert(gl->TexParameteri);
// optional
gl->GenerateMipmap = SDL_GL_GetProcAddress("glGenerateMipmap");
gl->GenerateMipmap = (void (*)(GLenum))
SDL_GL_GetProcAddress("glGenerateMipmap");
const char *version = (const char *) gl->GetString(GL_VERSION);
assert(version);

View File

@@ -4,7 +4,7 @@
#include "common.h"
#include <stdbool.h>
#include <SDL2/SDL_opengl.h>
#include <SDL3/SDL_opengl.h>
struct sc_opengl {
const char *version;
@@ -15,6 +15,9 @@ struct sc_opengl {
const GLubyte *
(*GetString)(GLenum name);
void
(*BindTexture)(GLenum target, GLuint texture);
void
(*TexParameterf)(GLenum target, GLenum pname, GLfloat param);

View File

@@ -2,7 +2,8 @@
#include <assert.h>
#include <inttypes.h>
#include <SDL2/SDL_clipboard.h>
#include <stdlib.h>
#include <SDL3/SDL_clipboard.h>
#include "device_msg.h"
#include "events.h"
@@ -53,8 +54,12 @@ task_set_clipboard(void *userdata) {
if (same) {
LOGD("Computer clipboard unchanged");
} else {
LOGI("Device clipboard copied");
SDL_SetClipboardText(text);
bool ok = SDL_SetClipboardText(text);
if (ok) {
LOGI("Device clipboard copied");
} else {
LOGE("Could not set clipboard: %s", SDL_GetError());
}
}
free(text);

View File

@@ -6,7 +6,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#ifdef _WIN32
// not needed here, but winsock2.h must never be included AFTER windows.h
@@ -93,8 +93,8 @@ struct scrcpy {
#ifdef _WIN32
static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_C_EVENT) {
sc_push_event(SDL_QUIT);
if (ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT) {
sc_push_event(SDL_EVENT_QUIT);
return TRUE;
}
return FALSE;
@@ -107,9 +107,9 @@ sdl_set_hints(const char *render_driver) {
LOGW("Could not set render driver");
}
// Linear filtering
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering");
// App name used in various contexts (such as PulseAudio)
if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) {
LOGW("Could not set app name");
}
// Handle a click to gain focus as any other click
@@ -117,21 +117,17 @@ sdl_set_hints(const char *render_driver) {
LOGW("Could not enable mouse focus clickthrough");
}
#ifdef SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
// Disable synthetic mouse events from touch events
// Touch events with id SDL_TOUCH_MOUSEID are ignored anyway, but it is
// better not to generate them in the first place.
if (!SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0")) {
LOGW("Could not disable synthetic mouse events");
}
#endif
#ifdef SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
// Disable compositor bypassing on X11
if (!SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, "0")) {
LOGW("Could not disable X11 compositor bypass");
}
#endif
// Do not minimize on focus loss
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
@@ -158,14 +154,20 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
}
if (disable_screensaver) {
SDL_DisableScreenSaver();
bool ok = SDL_DisableScreenSaver();
if (!ok) {
LOGW("Could not disable screen saver");
}
} else {
SDL_EnableScreenSaver();
bool ok = SDL_EnableScreenSaver();
if (!ok) {
LOGW("Could not enable screen saver");
}
}
}
static enum scrcpy_exit_code
event_loop(struct scrcpy *s) {
event_loop(struct scrcpy *s, bool has_screen) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
@@ -187,7 +189,7 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_TIME_LIMIT_REACHED:
LOGI("Time limit reached");
return SCRCPY_EXIT_SUCCESS;
case SDL_QUIT:
case SDL_EVENT_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
case SC_EVENT_RUN_ON_MAIN_THREAD: {
@@ -197,7 +199,7 @@ event_loop(struct scrcpy *s) {
break;
}
default:
if (!sc_screen_handle_event(&s->screen, &event)) {
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
}
break;
@@ -227,7 +229,7 @@ await_for_server(bool *connected) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
case SDL_EVENT_QUIT:
if (connected) {
*connected = false;
}
@@ -353,14 +355,21 @@ scrcpy_generate_scid(void) {
static void
init_sdl_gamepads(void) {
// Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already
// Trigger a SDL_EVENT_GAMEPAD_ADDED event for all gamepads already
// connected
int num_joysticks = SDL_NumJoysticks();
for (int i = 0; i < num_joysticks; ++i) {
if (SDL_IsGameController(i)) {
int count;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&count);
if (!joysticks) {
LOGE("Could not list joysticks: %s", SDL_GetError());
return;
}
for (int i = 0; i < count; ++i) {
SDL_JoystickID joystick = joysticks[i];
if (SDL_IsGamepad(joystick)) {
SDL_Event event;
event.cdevice.type = SDL_CONTROLLERDEVICEADDED;
event.cdevice.which = i;
event.gdevice.type = SDL_EVENT_GAMEPAD_ADDED;
event.gdevice.which = i;
SDL_PushEvent(&event);
}
}
@@ -376,7 +385,7 @@ scrcpy(struct scrcpy_options *options) {
struct scrcpy *s = &scrcpy;
// Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) {
if (!SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
return SCRCPY_EXIT_FAILURE;
}
@@ -502,7 +511,7 @@ scrcpy(struct scrcpy_options *options) {
// --no-video-playback is passed so that clipboard synchronization
// still works.
// <https://github.com/Genymobile/scrcpy/issues/4418>
if (SDL_Init(SDL_INIT_VIDEO)) {
if (!SDL_Init(SDL_INIT_VIDEO)) {
// If it fails, it is an error only if video playback is enabled
if (options->video_playback) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
@@ -514,14 +523,14 @@ scrcpy(struct scrcpy_options *options) {
}
if (options->audio_playback) {
if (SDL_Init(SDL_INIT_AUDIO)) {
if (!SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end;
}
}
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (SDL_Init(SDL_INIT_GAMECONTROLLER)) {
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
goto end;
}
@@ -933,7 +942,7 @@ aoa_complete:
}
}
ret = event_loop(s);
ret = event_loop(s, options->window);
terminate_event_loop();
LOGD("quit...");

View File

@@ -2,12 +2,13 @@
#include <assert.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "events.h"
#include "icon.h"
#include "options.h"
#include "util/log.h"
#include "util/sdl.h"
#define DISPLAY_MARGINS 96
@@ -26,45 +27,25 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
return oriented_size;
}
// get the window size in a struct sc_size
static struct sc_size
get_window_size(const struct sc_screen *screen) {
int width;
int height;
SDL_GetWindowSize(screen->window, &width, &height);
struct sc_size size;
size.width = width;
size.height = height;
return size;
}
static struct sc_point
get_window_position(const struct sc_screen *screen) {
int x;
int y;
SDL_GetWindowPosition(screen->window, &x, &y);
struct sc_point point;
point.x = x;
point.y = y;
return point;
}
// set the window size to be applied when fullscreen is disabled
static void
set_window_size(struct sc_screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
static inline bool
is_windowed(struct sc_screen *screen) {
return !(SDL_GetWindowFlags(screen->window) & (SDL_WINDOW_FULLSCREEN
| SDL_WINDOW_MINIMIZED
| SDL_WINDOW_MAXIMIZED));
}
// get the preferred display bounds (i.e. the screen bounds with some margins)
static bool
get_preferred_display_bounds(struct sc_size *bounds) {
SDL_Rect rect;
if (SDL_GetDisplayUsableBounds(0, &rect)) {
SDL_DisplayID display = SDL_GetPrimaryDisplay();
if (!display) {
LOGW("Could not get primary display: %s", SDL_GetError());
return false;
}
bool ok = SDL_GetDisplayUsableBounds(display, &rect);
if (!ok) {
LOGW("Could not get display usable bounds: %s", SDL_GetError());
return false;
}
@@ -166,13 +147,10 @@ static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
int dw;
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct sc_size drawable_size = {dw, dh};
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
SDL_Rect *rect = &screen->rect;
@@ -208,6 +186,7 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->video);
assert(screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
@@ -225,7 +204,7 @@ sc_screen_render_novideo(struct sc_screen *screen) {
(void) res; // any error already logged
}
#if defined(__APPLE__) || defined(__WINDOWS__)
#if defined(__APPLE__) || defined(_WIN32)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@@ -235,18 +214,18 @@ sc_screen_render_novideo(struct sc_screen *screen) {
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int
static bool
event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data;
assert(screen->video);
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
sc_screen_render(screen, true);
}
return 0;
return true;
}
#endif
@@ -257,6 +236,7 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
(void) ctx;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
if (ctx->width <= 0 || ctx->width > 0xFFFF
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
@@ -264,19 +244,6 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
return false;
}
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
// screen->frame_size is never used before the event is pushed, and the
// event acts as a memory barrier so it is safe without mutex
screen->frame_size.width = ctx->width;
screen->frame_size.height = ctx->height;
// Post the event on the UI thread (the texture must be created from there)
bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE);
if (!ok) {
return false;
}
#ifndef NDEBUG
screen->open = true;
#endif
@@ -327,9 +294,7 @@ sc_screen_init(struct sc_screen *screen,
const struct sc_screen_params *params) {
screen->resize_pending = false;
screen->has_frame = false;
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->has_video_window = false;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@@ -360,7 +325,7 @@ sc_screen_init(struct sc_screen *screen,
}
}
uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
@@ -394,22 +359,31 @@ sc_screen_init(struct sc_screen *screen,
}
// The window will be positioned and sized on first video frame
screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags);
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
goto error_destroy_fps_counter;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
} else if (params->video) {
// just a warning
LOGW("Could not load icon");
} else {
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_fps_counter;
goto error_destroy_window;
}
SDL_Surface *icon_novideo = params->video ? NULL : icon;
@@ -449,7 +423,11 @@ sc_screen_init(struct sc_screen *screen,
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (screen->video) {
SDL_AddEventWatch(event_watcher, screen);
ok = SDL_AddEventWatch(event_watcher, screen);
if (!ok) {
LOGW("Could not add event watcher for continuous resizing: %s",
SDL_GetError());
}
}
#endif
@@ -490,13 +468,18 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
? screen->req.x : (int) SDL_WINDOWPOS_CENTERED;
int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED
? screen->req.y : (int) SDL_WINDOWPOS_CENTERED;
struct sc_point position = {
.x = x,
.y = y,
};
struct sc_size window_size =
get_initial_optimal_size(screen->content_size, screen->req.width,
screen->req.height);
set_window_size(screen, window_size);
SDL_SetWindowPosition(screen->window, x, y);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, window_size);
sc_sdl_set_window_position(screen->window, position);
if (screen->req.fullscreen) {
sc_screen_toggle_fullscreen(screen);
@@ -506,13 +489,13 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
sc_fps_counter_start(&screen->fps_counter);
}
SDL_ShowWindow(screen->window);
sc_sdl_show_window(screen->window);
sc_screen_update_content_rect(screen);
}
void
sc_screen_hide_window(struct sc_screen *screen) {
SDL_HideWindow(screen->window);
sc_sdl_hide_window(screen->window);
}
void
@@ -542,7 +525,7 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
struct sc_size new_content_size) {
assert(screen->video);
struct sc_size window_size = get_window_size(screen);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
/ old_content_size.width,
@@ -550,14 +533,15 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
/ old_content_size.height,
};
target_size = get_optimal_size(target_size, new_content_size, true);
set_window_size(screen, target_size);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, target_size);
}
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
assert(screen->video);
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
if (is_windowed(screen)) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once
@@ -573,9 +557,7 @@ static void
apply_pending_resize(struct sc_screen *screen) {
assert(screen->video);
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
assert(is_windowed(screen));
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
@@ -603,44 +585,6 @@ sc_screen_set_orientation(struct sc_screen *screen,
sc_screen_render(screen, true);
}
static bool
sc_screen_init_size(struct sc_screen *screen) {
// Before first frame
assert(!screen->has_frame);
// The requested size is passed via screen->frame_size
struct sc_size content_size =
get_oriented_size(screen->frame_size, screen->orientation);
screen->content_size = content_size;
enum sc_display_result res =
sc_display_set_texture_size(&screen->display, screen->frame_size);
return res != SC_DISPLAY_RESULT_ERROR;
}
// recreate the texture and resize the window if the frame size has changed
static enum sc_display_result
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
assert(screen->video);
if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) {
return SC_DISPLAY_RESULT_OK;
}
// frame dimension changed
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation);
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
return sc_display_set_texture_size(&screen->display, screen->frame_size);
}
static bool
sc_screen_apply_frame(struct sc_screen *screen) {
assert(screen->video);
@@ -649,7 +593,38 @@ sc_screen_apply_frame(struct sc_screen *screen) {
AVFrame *frame = screen->frame;
struct sc_size new_frame_size = {frame->width, frame->height};
enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
if (!screen->has_frame
|| screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_oriented_size(new_frame_size, screen->orientation);
if (screen->has_frame) {
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
} else {
// This is the first frame
screen->has_frame = true;
screen->content_size = new_content_size;
}
enum sc_display_result res =
sc_display_prepare_texture(&screen->display, screen->frame_size,
frame->colorspace, frame->color_range);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
}
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
@@ -658,17 +633,9 @@ sc_screen_apply_frame(struct sc_screen *screen) {
return true;
}
res = sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
if (!screen->has_frame) {
screen->has_frame = true;
assert(screen->has_frame);
if (!screen->has_video_window) {
screen->has_video_window = true;
// this is the very first frame, show the window
sc_screen_show_initial_window(screen);
@@ -738,31 +705,28 @@ void
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
assert(screen->video);
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
bool req_fullscreen =
!(SDL_GetWindowFlags(screen->window) & SDL_WINDOW_FULLSCREEN);
bool ok = SDL_SetWindowFullscreen(screen->window, req_fullscreen);
if (!ok) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
return;
}
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
apply_pending_resize(screen);
}
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
sc_screen_render(screen, true);
LOGD("Requested %s mode", req_fullscreen ? "fullscreen" : "windowed");
}
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->maximized || screen->minimized) {
if (!is_windowed(screen)) {
return;
}
struct sc_point point = get_window_position(screen);
struct sc_size window_size = get_window_size(screen);
struct sc_point point = sc_sdl_get_window_position(screen->window);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
struct sc_size optimal_size =
get_optimal_size(window_size, screen->content_size, false);
@@ -770,11 +734,14 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
// Center the window related to the device screen
assert(optimal_size.width <= window_size.width);
assert(optimal_size.height <= window_size.height);
uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2;
uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2;
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
SDL_SetWindowPosition(screen->window, new_x, new_y);
struct sc_point new_position = {
.x = point.x + (window_size.width - optimal_size.width) / 2,
.y = point.y + (window_size.height - optimal_size.height) / 2,
};
sc_sdl_set_window_size(screen->window, optimal_size);
sc_sdl_set_window_position(screen->window, new_position);
LOGD("Resized to optimal size: %ux%u", optimal_size.width,
optimal_size.height);
}
@@ -783,33 +750,21 @@ void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->minimized) {
if (!is_windowed(screen)) {
return;
}
if (screen->maximized) {
SDL_RestoreWindow(screen->window);
screen->maximized = false;
}
struct sc_size content_size = screen->content_size;
SDL_SetWindowSize(screen->window, content_size.width, content_size.height);
sc_sdl_set_window_size(screen->window, content_size);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
}
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
// !video implies !has_video_window
assert(screen->video || !screen->has_video_window);
switch (event->type) {
case SC_EVENT_SCREEN_INIT_SIZE: {
// The initial size is passed via screen->frame_size
bool ok = sc_screen_init_size(screen);
if (!ok) {
LOGE("Could not initialize screen size");
return false;
}
return true;
}
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
@@ -818,45 +773,34 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
}
return true;
}
case SDL_WINDOWEVENT:
if (!screen->video
&& event->window.event == SDL_WINDOWEVENT_EXPOSED) {
case SDL_EVENT_WINDOW_EXPOSED:
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
sc_screen_render(screen, true);
}
// !video implies !has_frame
assert(screen->video || !screen->has_frame);
if (!screen->has_frame) {
// Do nothing
return true;
return true;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (screen->has_video_window) {
sc_screen_render(screen, true);
}
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
sc_screen_render(screen, true);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
sc_screen_render(screen, true);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;
case SDL_WINDOWEVENT_MINIMIZED:
screen->minimized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling
// fullscreen mode unexpectedly triggers the "restored"
// then "maximized" events, leaving the window in a
// weird state (maximized according to the events, but
// not maximized visually).
break;
}
screen->maximized = false;
screen->minimized = false;
apply_pending_resize(screen);
sc_screen_render(screen, true);
break;
return true;
case SDL_EVENT_WINDOW_RESTORED:
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return true;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->has_video_window);
return true;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->has_video_window);
if (is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return true;
}
@@ -937,9 +881,15 @@ sc_screen_convert_window_to_frame_coords(struct sc_screen *screen,
void
sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) {
// take the HiDPI scaling (dw/ww and dh/wh) into account
int ww, wh, dw, dh;
SDL_GetWindowSize(screen->window, &ww, &wh);
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
struct sc_size window_size = sc_sdl_get_window_size(screen->window);
int64_t ww = window_size.width;
int64_t wh = window_size.height;
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
int64_t dw = drawable_size.width;
int64_t dh = drawable_size.height;
// scale for HiDPI (64 bits for intermediate multiplications)
*x = (int64_t) *x * dw / ww;

View File

@@ -5,7 +5,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include <libavcodec/avcodec.h>
#include <libavutil/frame.h>
#include <libavutil/pixfmt.h>
@@ -61,9 +61,7 @@ struct sc_screen {
// rectangle of the content (excluding black borders)
struct SDL_Rect rect;
bool has_frame;
bool fullscreen;
bool maximized;
bool minimized;
bool has_video_window;
AVFrame *frame;

View File

@@ -6,11 +6,11 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_keycode.h>
#include <SDL3/SDL_keycode.h>
#include "options.h"
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
#define SC_SDL_SHORTCUT_MODS_MASK (SDL_KMOD_CTRL | SDL_KMOD_ALT | SDL_KMOD_GUI)
// input: OR of enum sc_shortcut_mod
// output: OR of SDL_Keymod
@@ -18,22 +18,22 @@ static inline uint16_t
sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) {
uint16_t sdl_mod = 0;
if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
sdl_mod |= SDL_KMOD_LCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
sdl_mod |= SDL_KMOD_RCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LALT) {
sdl_mod |= KMOD_LALT;
sdl_mod |= SDL_KMOD_LALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RALT) {
sdl_mod |= KMOD_RALT;
sdl_mod |= SDL_KMOD_RALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
sdl_mod |= SDL_KMOD_LGUI;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
sdl_mod |= SDL_KMOD_RGUI;
}
return sdl_mod;
}
@@ -50,12 +50,12 @@ sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) {
static inline bool
sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods,
SDL_Keycode keycode) {
return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
return (sdl_shortcut_mods & SDL_KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (sdl_shortcut_mods & SDL_KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (sdl_shortcut_mods & SDL_KMOD_LALT && keycode == SDLK_LALT)
|| (sdl_shortcut_mods & SDL_KMOD_RALT && keycode == SDLK_RALT)
|| (sdl_shortcut_mods & SDL_KMOD_LGUI && keycode == SDLK_LGUI)
|| (sdl_shortcut_mods & SDL_KMOD_RGUI && keycode == SDLK_RGUI);
}
#endif

View File

@@ -3,7 +3,7 @@
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <SDL2/SDL_gamecontroller.h>
#include <SDL3/SDL_gamepad.h>
#include "hid/hid_gamepad.h"
#include "input_events.h"
@@ -74,10 +74,9 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
return;
}
SDL_GameController* game_controller =
SDL_GameControllerFromInstanceID(event->gamepad_id);
assert(game_controller);
const char *name = SDL_GameControllerName(game_controller);
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(event->gamepad_id);
assert(sdl_gamepad);
const char *name = SDL_GetGamepadName(sdl_gamepad);
LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name);
sc_gamepad_uhid_send_open(gamepad, &hid_open);

View File

@@ -2,8 +2,8 @@
#include <assert.h>
#include <string.h>
#include <SDL2/SDL_keyboard.h>
#include <SDL2/SDL_keycode.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_keycode.h>
#include "util/log.h"
#include "util/thread.h"

View File

@@ -55,7 +55,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
if (!sc_hid_mouse_generate_input_from_scroll(&mouse->hid, &hid_input,
event)) {
return;
}
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
}
@@ -63,6 +66,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
bool
sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
struct sc_controller *controller) {
sc_hid_mouse_init(&mouse->hid);
mouse->controller = controller;
static const struct sc_mouse_processor_ops ops = {

View File

@@ -4,11 +4,13 @@
#include <stdbool.h>
#include "controller.h"
#include "hid/hid_mouse.h"
#include "trait/mouse_processor.h"
struct sc_mouse_uhid {
struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_hid_mouse hid;
struct sc_controller *controller;
};

View File

@@ -42,7 +42,10 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
if (!sc_hid_mouse_generate_input_from_scroll(&mouse->hid, &hid_input,
event)) {
return;
}
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse scroll)");
@@ -62,6 +65,8 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) {
return false;
}
sc_hid_mouse_init(&mouse->hid);
static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion,
.process_mouse_click = sc_mouse_processor_process_mouse_click,

View File

@@ -6,11 +6,13 @@
#include <stdbool.h>
#include "usb/aoa_hid.h"
#include "hid/hid_mouse.h"
#include "trait/mouse_processor.h"
struct sc_mouse_aoa {
struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_hid_mouse hid;
struct sc_aoa *aoa;
};

View File

@@ -3,7 +3,7 @@
#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#ifdef _WIN32
# include "adb/adb.h"
@@ -45,7 +45,7 @@ event_loop(struct scrcpy_otg *s) {
case SC_EVENT_AOA_OPEN_ERROR:
LOGE("AOA open error");
return SCRCPY_EXIT_FAILURE;
case SDL_QUIT:
case SDL_EVENT_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
default:
@@ -63,23 +63,19 @@ scrcpy_otg(struct scrcpy_options *options) {
const char *serial = options->serial;
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) {
LOGW("Could not enable linear filtering");
}
if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) {
LOGW("Could not allow joystick background events");
}
// Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) {
if (!SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
return SCRCPY_EXIT_FAILURE;
}
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (SDL_Init(SDL_INIT_GAMECONTROLLER)) {
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
// Not fatal, keyboard/mouse should still work
}
}

View File

@@ -7,14 +7,19 @@
#include "options.h"
#include "util/acksync.h"
#include "util/log.h"
#include "util/sdl.h"
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
SDL_RenderClear(screen->renderer);
sc_sdl_render_clear(screen->renderer);
if (screen->texture) {
SDL_RenderCopy(screen->renderer, screen->texture, NULL, NULL);
bool ok =
SDL_RenderTexture(screen->renderer, screen->texture, NULL, NULL);
if (!ok) {
LOGW("Could not render texture: %s", SDL_GetError());
}
}
SDL_RenderPresent(screen->renderer);
sc_sdl_render_present(screen->renderer);
}
bool
@@ -34,7 +39,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
int width = params->window_width ? params->window_width : 256;
int height = params->window_height ? params->window_height : 256;
uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
@@ -42,13 +47,14 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags);
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
return false;
}
screen->renderer = SDL_CreateRenderer(screen->window, -1, 0);
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
@@ -57,9 +63,15 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
bool ok = SDL_SetWindowIcon(screen->window, icon);
if (!ok) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
if (SDL_RenderSetLogicalSize(screen->renderer, icon->w, icon->h)) {
ok = SDL_SetRenderLogicalPresentation(screen->renderer, icon->w,
icon->h,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (!ok) {
LOGW("Could not set renderer logical size: %s", SDL_GetError());
// don't fail
}
@@ -108,10 +120,10 @@ sc_screen_otg_process_key(struct sc_screen_otg *screen,
struct sc_key_event evt = {
.action = sc_action_from_sdl_keyboard_type(event->type),
.keycode = sc_keycode_from_sdl(event->keysym.sym),
.scancode = sc_scancode_from_sdl(event->keysym.scancode),
.keycode = sc_keycode_from_sdl(event->key),
.scancode = sc_scancode_from_sdl(event->scancode),
.repeat = event->repeat,
.mods_state = sc_mods_state_from_sdl(event->keysym.mod),
.mods_state = sc_mods_state_from_sdl(event->mod),
};
assert(kp->ops->process_key);
@@ -175,34 +187,34 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
static void
sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
const SDL_ControllerDeviceEvent *event) {
const SDL_GamepadDeviceEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) {
LOGW("Could not open game controller");
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
return;
}
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc);
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (!joystick) {
LOGW("Could not get controller joystick");
SDL_GameControllerClose(gc);
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
return;
}
struct sc_gamepad_device_event evt = {
.gamepad_id = SDL_JoystickInstanceID(joystick),
.gamepad_id = SDL_GetJoystickID(joystick),
};
gp->ops->process_gamepad_added(gp, &evt);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) {
SDL_GameControllerClose(gc);
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
} else {
LOGW("Unknown gamepad device removed");
}
@@ -216,7 +228,7 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
static void
sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
const SDL_ControllerAxisEvent *event) {
const SDL_GamepadAxisEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
@@ -235,7 +247,7 @@ sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
static void
sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
const SDL_ControllerButtonEvent *event) {
const SDL_GamepadButtonEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
@@ -246,7 +258,7 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.button = button,
};
gp->ops->process_gamepad_button(gp, &evt);
@@ -260,59 +272,55 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
}
switch (event->type) {
case SDL_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
sc_screen_otg_render(screen);
break;
}
return;
case SDL_KEYDOWN:
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_otg_render(screen);
break;
case SDL_EVENT_KEY_DOWN:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_KEYUP:
case SDL_EVENT_KEY_UP:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_MOUSEMOTION:
case SDL_EVENT_MOUSE_MOTION:
if (screen->mouse) {
sc_screen_otg_process_mouse_motion(screen, &event->motion);
}
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_MOUSEBUTTONUP:
case SDL_EVENT_MOUSE_BUTTON_UP:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_MOUSEWHEEL:
case SDL_EVENT_MOUSE_WHEEL:
if (screen->mouse) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
// Handle device added or removed even if paused
if (screen->gamepad) {
sc_screen_otg_process_gamepad_device(screen, &event->cdevice);
sc_screen_otg_process_gamepad_device(screen, &event->gdevice);
}
break;
case SDL_CONTROLLERAXISMOTION:
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_axis(screen, &event->caxis);
sc_screen_otg_process_gamepad_axis(screen, &event->gaxis);
}
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_button(screen, &event->cbutton);
sc_screen_otg_process_gamepad_button(screen, &event->gbutton);
}
break;
}

View File

@@ -5,7 +5,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL.h>
#include <SDL3/SDL.h>
#include "mouse_capture.h"
#include "usb/gamepad_aoa.h"

View File

@@ -50,13 +50,13 @@ log_level_sdl_to_sc(SDL_LogPriority priority) {
void
sc_set_log_level(enum sc_log_level level) {
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
SDL_SetLogPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
}
enum sc_log_level
sc_get_log_level(void) {
SDL_LogPriority sdl_log = SDL_LogGetPriority(SDL_LOG_CATEGORY_APPLICATION);
SDL_LogPriority sdl_log = SDL_GetLogPriority(SDL_LOG_CATEGORY_APPLICATION);
return log_level_sdl_to_sc(sdl_log);
}
@@ -128,7 +128,7 @@ sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
free(local_fmt);
}
static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = {
static const char *const sc_sdl_log_priority_names[SDL_LOG_PRIORITY_COUNT] = {
[SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE",
[SDL_LOG_PRIORITY_DEBUG] = "DEBUG",
[SDL_LOG_PRIORITY_INFO] = "INFO",
@@ -144,14 +144,14 @@ sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
(void) category;
FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr;
assert(priority < SDL_NUM_LOG_PRIORITIES);
assert(priority < SDL_LOG_PRIORITY_COUNT);
const char *prio_name = sc_sdl_log_priority_names[priority];
fprintf(out, "%s: %s\n", prio_name, message);
}
void
sc_log_configure(void) {
SDL_LogSetOutputFunction(sc_sdl_log_print, NULL);
SDL_SetLogOutputFunction(sc_sdl_log_print, NULL);
// Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback);
}

View File

@@ -3,7 +3,7 @@
#include "common.h"
#include <SDL2/SDL_log.h>
#include <SDL3/SDL_log.h>
#include "options.h"

View File

@@ -2,6 +2,7 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
# include <ws2tcpip.h>

149
app/src/util/sdl.c Normal file
View File

@@ -0,0 +1,149 @@
#include "sdl.h"
#include <assert.h>
#include <stdlib.h>
#include "util/log.h"
SDL_Window *
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags) {
SDL_Window *window = NULL;
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
bool ok =
SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
title);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, x);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, y);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER,
width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER,
height);
ok &= SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_FLAGS_NUMBER,
flags);
if (!ok) {
SDL_DestroyProperties(props);
return NULL;
}
window = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props);
return window;
}
struct sc_size
sc_sdl_get_window_size(SDL_Window *window) {
int width;
int height;
bool ok = SDL_GetWindowSize(window, &width, &height);
if (!ok) {
LOGE("Could not get window size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
struct sc_size
sc_sdl_get_window_size_in_pixels(SDL_Window *window) {
int width;
int height;
bool ok = SDL_GetWindowSizeInPixels(window, &width, &height);
if (!ok) {
LOGE("Could not get window size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
void
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size) {
bool ok = SDL_SetWindowSize(window, size.width, size.height);
if (!ok) {
LOGE("Could not set window size: %s", SDL_GetError());
assert(!"unexpected");
}
}
struct sc_point
sc_sdl_get_window_position(SDL_Window *window) {
int x;
int y;
bool ok = SDL_GetWindowPosition(window, &x, &y);
if (!ok) {
LOGE("Could not get window position: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_point point = {
.x = x,
.y = y,
};
return point;
}
void
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point) {
bool ok = SDL_SetWindowPosition(window, point.x, point.y);
if (!ok) {
LOGE("Could not set window position: %s", SDL_GetError());
assert(!"unexpected");
}
}
void
sc_sdl_show_window(SDL_Window *window) {
bool ok = SDL_ShowWindow(window);
if (!ok) {
LOGE("Could not show window: %s", SDL_GetError());
assert(!"unexpected");
}
}
void
sc_sdl_hide_window(SDL_Window *window) {
bool ok = SDL_HideWindow(window);
if (!ok) {
LOGE("Could not hide window: %s", SDL_GetError());
assert(!"unexpected");
}
}
bool
sc_sdl_render_clear(SDL_Renderer *renderer) {
bool ok = SDL_RenderClear(renderer);
if (!ok) {
LOGW("Could not clear rendering: %s", SDL_GetError());
}
return ok;
}
void
sc_sdl_render_present(SDL_Renderer *renderer) {
bool ok = SDL_RenderPresent(renderer);
if (!ok) {
LOGE("Could not render: %s", SDL_GetError());
assert(!"unexpected");
}
}

43
app/src/util/sdl.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef SC_SDL_H
#define SC_SDL_H
#include "common.h"
#include <stdint.h>
#include <SDL3/SDL_render.h>
#include <SDL3/SDL_video.h>
#include "coords.h"
SDL_Window *
sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
int64_t height, int64_t flags);
struct sc_size
sc_sdl_get_window_size(SDL_Window *window);
struct sc_size
sc_sdl_get_window_size_in_pixels(SDL_Window *window);
void
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size);
struct sc_point
sc_sdl_get_window_position(SDL_Window *window);
void
sc_sdl_set_window_position(SDL_Window *window, struct sc_point point);
void
sc_sdl_show_window(SDL_Window *window);
void
sc_sdl_hide_window(SDL_Window *window);
bool
sc_sdl_render_clear(SDL_Renderer *renderer);
void
sc_sdl_render_present(SDL_Renderer *renderer);
#endif

View File

@@ -4,7 +4,7 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL_thread.h>
#include <SDL3/SDL_mutex.h>
#include "util/log.h"
@@ -31,11 +31,7 @@ static SDL_ThreadPriority
to_sdl_thread_priority(enum sc_thread_priority priority) {
switch (priority) {
case SC_THREAD_PRIORITY_TIME_CRITICAL:
#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
return SDL_THREAD_PRIORITY_TIME_CRITICAL;
#else
// fall through
#endif
case SC_THREAD_PRIORITY_HIGH:
return SDL_THREAD_PRIORITY_HIGH;
case SC_THREAD_PRIORITY_NORMAL:
@@ -51,8 +47,8 @@ to_sdl_thread_priority(enum sc_thread_priority priority) {
bool
sc_thread_set_priority(enum sc_thread_priority priority) {
SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority);
int r = SDL_SetThreadPriority(sdl_priority);
if (r) {
bool ok = SDL_SetCurrentThreadPriority(sdl_priority);
if (!ok) {
LOGD("Could not set thread priority: %s", SDL_GetError());
return false;
}
@@ -67,7 +63,7 @@ sc_thread_join(sc_thread *thread, int *status) {
bool
sc_mutex_init(sc_mutex *mutex) {
SDL_mutex *sdl_mutex = SDL_CreateMutex();
SDL_Mutex *sdl_mutex = SDL_CreateMutex();
if (!sdl_mutex) {
LOG_OOM();
return false;
@@ -89,40 +85,25 @@ void
sc_mutex_lock(sc_mutex *mutex) {
// SDL mutexes are recursive, but we don't want to use recursive mutexes
assert(!sc_mutex_held(mutex));
int r = SDL_LockMutex(mutex->mutex);
SDL_LockMutex(mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGE("Could not lock mutex: %s", SDL_GetError());
abort();
}
atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
memory_order_relaxed);
#else
(void) r;
#endif
}
void
sc_mutex_unlock(sc_mutex *mutex) {
#ifndef NDEBUG
assert(sc_mutex_held(mutex));
#ifndef NDEBUG
atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed);
#endif
int r = SDL_UnlockMutex(mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGE("Could not lock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
SDL_UnlockMutex(mutex->mutex);
}
sc_thread_id
sc_thread_get_id(void) {
return SDL_ThreadID();
return SDL_GetCurrentThreadID();
}
#ifndef NDEBUG
@@ -136,7 +117,7 @@ sc_mutex_held(struct sc_mutex *mutex) {
bool
sc_cond_init(sc_cond *cond) {
SDL_cond *sdl_cond = SDL_CreateCond();
SDL_Condition *sdl_cond = SDL_CreateCondition();
if (!sdl_cond) {
LOG_OOM();
return false;
@@ -148,22 +129,15 @@ sc_cond_init(sc_cond *cond) {
void
sc_cond_destroy(sc_cond *cond) {
SDL_DestroyCond(cond->cond);
SDL_DestroyCondition(cond->cond);
}
void
sc_cond_wait(sc_cond *cond, sc_mutex *mutex) {
int r = SDL_CondWait(cond->cond, mutex->mutex);
SDL_WaitCondition(cond->cond, mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGE("Could not wait on condition: %s", SDL_GetError());
abort();
}
atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
memory_order_relaxed);
#else
(void) r;
#endif
}
@@ -177,44 +151,22 @@ sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) {
// Round up to the next millisecond to guarantee that the deadline is
// reached when returning due to timeout
uint32_t ms = SC_TICK_TO_MS(deadline - now + SC_TICK_FROM_MS(1) - 1);
int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms);
bool signaled = SDL_WaitConditionTimeout(cond->cond, mutex->mutex, ms);
#ifndef NDEBUG
if (r < 0) {
LOGE("Could not wait on condition with timeout: %s", SDL_GetError());
abort();
}
atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
memory_order_relaxed);
#endif
assert(r == 0 || r == SDL_MUTEX_TIMEDOUT);
// The deadline is reached on timeout
assert(r != SDL_MUTEX_TIMEDOUT || sc_tick_now() >= deadline);
return r == 0;
assert(signaled || sc_tick_now() >= deadline);
return signaled;
}
void
sc_cond_signal(sc_cond *cond) {
int r = SDL_CondSignal(cond->cond);
#ifndef NDEBUG
if (r) {
LOGE("Could not signal a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
SDL_SignalCondition(cond->cond);
}
void
sc_cond_broadcast(sc_cond *cond) {
int r = SDL_CondBroadcast(cond->cond);
#ifndef NDEBUG
if (r) {
LOGE("Could not broadcast a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
SDL_BroadcastCondition(cond->cond);
}

View File

@@ -10,8 +10,8 @@
/* Forward declarations */
typedef struct SDL_Thread SDL_Thread;
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
typedef struct SDL_Mutex SDL_Mutex;
typedef struct SDL_Condition SDL_Condition;
typedef int sc_thread_fn(void *);
typedef unsigned sc_thread_id;
@@ -29,14 +29,14 @@ enum sc_thread_priority {
};
typedef struct sc_mutex {
SDL_mutex *mutex;
SDL_Mutex *mutex;
#ifndef NDEBUG
sc_atomic_thread_id locker;
#endif
} sc_mutex;
typedef struct sc_cond {
SDL_cond *cond;
SDL_Condition *cond;
} sc_cond;
extern sc_thread_id SC_MAIN_THREAD_ID;

View File

@@ -191,7 +191,8 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
size_t right_len = MIN(size, oldcap - oldorigin);
assert(right_len);
memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size);
memcpy(newptr, (char *) ptr + (oldorigin * item_size),
right_len * item_size);
if (size > right_len) {
memcpy((char *) newptr + (right_len * item_size), ptr,

View File

@@ -10,17 +10,20 @@
#ifdef HAVE_USB
# include <libusb-1.0/libusb.h>
#endif
#include <SDL2/SDL_version.h>
#include <SDL3/SDL_version.h>
void
scrcpy_print_version(void) {
printf("\nDependencies (compiled / linked):\n");
SDL_version sdl;
SDL_GetVersion(&sdl);
int sdl = SDL_GetVersion();
printf(" - SDL: %u.%u.%u / %u.%u.%u\n",
SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_PATCHLEVEL,
(unsigned) sdl.major, (unsigned) sdl.minor, (unsigned) sdl.patch);
SDL_MAJOR_VERSION,
SDL_MINOR_VERSION,
SDL_MICRO_VERSION,
SDL_VERSIONNUM_MAJOR(sdl),
SDL_VERSIONNUM_MINOR(sdl),
SDL_VERSIONNUM_MICRO(sdl));
unsigned avcodec = avcodec_version();
printf(" - libavcodec: %u.%u.%u / %u.%u.%u\n",

View File

@@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) {
.height = 1920,
},
},
.hscroll = 1,
.vscroll = -1,
.hscroll = 16,
.vscroll = -16,
.buttons = 1,
},
};
@@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x7F, 0xFF, // 1 (float encoded as i16)
0x80, 0x00, // -1 (float encoded as i16)
0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16])
0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16])
0x00, 0x00, 0x00, 0x01, // 1
};
assert(!memcmp(buf, expected, sizeof(expected)));
@@ -411,6 +411,26 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_start_app(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_START_APP,
.start_app = {
.name = "firefox",
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 9);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_START_APP,
7, // length
'f', 'i', 'r', 'e', 'f', 'o', 'x', // app name
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
@@ -448,6 +468,7 @@ int main(int argc, char *argv[]) {
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
test_serialize_start_app();
test_serialize_reset_video();
return 0;
}

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.1'
classpath 'com.android.tools.build:gradle:8.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

73
container/Dockerfile Normal file
View File

@@ -0,0 +1,73 @@
# Build with:
# podman build -t scrcpy-builder .
#
# Run with:
# podman run \
# -v PATH_TO_SCRCPY:/home/debian/scrcpy \
# --userns=keep-id \
# -it scrcpy-builder \
# bash
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
sudo wget unzip gcc git pkg-config meson ninja-build cmake \
libsdl3-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0-dev openjdk-21-jdk \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
RUN groupadd -g 1000 debian \
&& useradd -m -u 1000 -g 1000 -s /bin/bash debian
RUN echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
ENV ANDROID_HOME=/opt/android/sdk
RUN mkdir -p "$ANDROID_HOME" \
&& chown debian:debian "$ANDROID_HOME"
USER debian
WORKDIR /home/debian
ENV CMDLINETOOLS_URL=https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
ENV CMDLINETOOLS_SHA256=7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee
RUN wget -q "$CMDLINETOOLS_URL" -O cmdlinetools.zip \
&& echo "$CMDLINETOOLS_SHA256 cmdlinetools.zip" | sha256sum -c
RUN mkdir tmp \
&& unzip -q cmdlinetools.zip -d tmp \
&& mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& mv tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" \
&& rmdir tmp \
&& rm cmdlinetools.zip
# To get the licence hash, run manually: "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses
# Build-tools 35 is still needed for the current AGP version
RUN mkdir -p "$ANDROID_HOME/licenses" \
&& echo 24333f8a63b6825ea9c5514f83c2829b004d1fee > "$ANDROID_HOME/licenses/android-sdk-license" \
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-36" "build-tools;35.0.0" "build-tools;36.0.0"
# For scrcpy build scripts
ENV GRADLE_VERSION=8.14.3
ENV GRADLE_HOME=/opt/gradle-$GRADLE_VERSION
ENV GRADLE_URL=https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip
ENV GRADLE_SHA256=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
ENV GRADLE=gradle
ENV PATH=$GRADLE_HOME/bin:$PATH
USER root
RUN wget -q "$GRADLE_URL" -O gradle.zip \
&& echo "$GRADLE_SHA256 gradle.zip" | sha256sum -c
RUN unzip -q gradle.zip -d /opt \
&& rm gradle.zip
USER debian
# Pre-download gradle dependencies for scrcpy
RUN mkdir -p /home/debian/fake-scrcpy/app
COPY fake.gradle /home/debian/fake-scrcpy/build.gradle
COPY fake_app.gradle /home/debian/fake-scrcpy/app/build.gradle
RUN echo "include ':app'" > /home/debian/fake-scrcpy/settings.gradle \
&& gradle -p /home/debian/fake-scrcpy dependencies androidDependencies --no-daemon \
&& rm -rf /home/debian/fake-scrcpy

17
container/fake.gradle Normal file
View File

@@ -0,0 +1,17 @@
// Fake build.gradle to pre-download gradle dependencies
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}

21
container/fake_app.gradle Normal file
View File

@@ -0,0 +1,21 @@
// Fake app/build.gradle to pre-download gradle dependencies
apply plugin: 'com.android.application'
apply plugin: 'checkstyle'
android {
buildToolsVersion = "36.0.0"
namespace = "com.genymobile.scrcpy"
compileSdk 36
defaultConfig {
minSdkVersion 21
targetSdkVersion 36
}
}
checkstyle {
toolVersion = '10.12.5'
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}

View File

@@ -30,13 +30,13 @@ the following files to a directory accessible from your `PATH`:
It is also available in scrcpy releases.
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
The client requires [FFmpeg] and [SDL]. Just follow the instructions.
[adb]: https://developer.android.com/studio/command-line/adb.html
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
[SDL]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
@@ -50,10 +50,10 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
sudo apt install ffmpeg libsdl3-0 adb libusb-1.0-0
# client build dependencies
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
sudo apt install gcc git pkg-config meson ninja-build libsdl3-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0-dev
@@ -77,7 +77,7 @@ pip3 install meson
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies
sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
sudo dnf install SDL3-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
@@ -121,7 +121,7 @@ install the required packages:
```bash
# runtime dependencies
pacman -S mingw-w64-x86_64-SDL2 \
pacman -S mingw-w64-x86_64-sdl3 \
mingw-w64-x86_64-ffmpeg \
mingw-w64-x86_64-libusb
@@ -136,7 +136,7 @@ For a 32 bits version, replace `x86_64` by `i686`:
```bash
# runtime dependencies
pacman -S mingw-w64-i686-SDL2 \
pacman -S mingw-w64-i686-sdl3 \
mingw-w64-i686-ffmpeg \
mingw-w64-i686-libusb
@@ -162,7 +162,7 @@ Install the packages with [Homebrew]:
```bash
# runtime dependencies
brew install sdl2 ffmpeg libusb
brew install sdl3 ffmpeg libusb
# client build dependencies
brew install pkg-config meson
@@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v3.2`][direct-scrcpy-server]
<sub>SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0`</sub>
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
@@ -271,7 +271,7 @@ This installs several files:
- `/usr/local/bin/scrcpy` (main app)
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
- `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
- `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon)
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)

View File

@@ -6,11 +6,11 @@
Download a static build of the [latest release]:
- [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be`</sub>
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
and extract it.
@@ -39,8 +39,8 @@ First, you need to install the required packages:
```bash
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
sudo apt install ffmpeg libsdl3-0 adb wget \
gcc git pkg-config meson ninja-build libsdl3-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
```

View File

@@ -6,15 +6,14 @@
Download a static build of the [latest release]:
- [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b`</sub>
- [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e`</sub>
- [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`</sub>
- [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.tar.gz
and extract it.

View File

@@ -6,14 +6,14 @@
Download the [latest release]:
- [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit)
<sub>SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0`</sub>
- [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit)
<sub>SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357`</sub>
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub>
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
and extract it.

View File

@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
# https://gradle.org/release-checksums/
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionSha256Sum=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2
PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

@@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '3.2',
version: '3.3.4',
meson_version: '>= 0.49',
default_options: [
'c_std=c11',

View File

@@ -1,15 +1,15 @@
apply plugin: 'com.android.application'
android {
namespace 'com.genymobile.scrcpy'
compileSdk 35
namespace = 'com.genymobile.scrcpy'
compileSdk 36
defaultConfig {
applicationId "com.genymobile.scrcpy"
applicationId = "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 35
versionCode 30200
versionName "3.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
targetSdkVersion 36
versionCode 30304
versionName "3.3.4"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
@@ -18,8 +18,11 @@ android {
}
}
buildFeatures {
buildConfig true
aidl true
buildConfig = true
aidl = true
}
lint {
disable 'UseRequiresApi'
}
}

View File

@@ -12,10 +12,10 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.2
SCRCPY_VERSION_NAME=3.3.4
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
PLATFORM=${ANDROID_PLATFORM:-36}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
@@ -86,7 +86,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
echo "Dexing..."
cd "$CLASSES_DIR"
if [[ $PLATFORM -lt 31 ]]
if [[ "${PLATFORM%%.*}" -lt 31 ]]
then
# use dx
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \

View File

@@ -48,19 +48,4 @@ oneway interface IDisplayWindowListener {
* Called when a display is removed from the hierarchy.
*/
void onDisplayRemoved(int displayId);
/**
* Called when fixed rotation is started on a display.
*/
void onFixedRotationStarted(int displayId, int newRotation);
/**
* Called when the previous fixed rotation on a display is finished.
*/
void onFixedRotationFinished(int displayId);
/**
* Called when the keep clear ares on a display have changed.
*/
void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted);
}

View File

@@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.BatteryManager;
import android.os.Looper;
import android.system.ErrnoException;
import android.system.Os;
@@ -179,6 +180,11 @@ public final class CleanUp {
}
}
@SuppressWarnings("deprecation")
private static void prepareMainLooper() {
Looper.prepareMainLooper();
}
public static void main(String... args) {
try {
// Start a new session to avoid being terminated along with the server process on some devices
@@ -188,6 +194,10 @@ public final class CleanUp {
}
unlinkSelf();
// Needed for workarounds
prepareMainLooper();
Workarounds.apply();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);

View File

@@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContentResolver;
@@ -11,6 +12,8 @@ import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process;
import java.lang.reflect.Field;
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
@@ -72,7 +75,7 @@ public final class FakeContext extends ContextWrapper {
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName("shell");
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
@@ -87,8 +90,38 @@ public final class FakeContext extends ContextWrapper {
return this;
}
@Override
public Context createPackageContext(String packageName, int flags) {
return this;
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
@SuppressLint("SoonBlockedPrivateApi")
@Override
public Object getSystemService(String name) {
Object service = super.getSystemService(name);
if (service == null) {
return null;
}
// "semclipboard" is a Samsung-internal service
// See:
// - <https://github.com/Genymobile/scrcpy/issues/6224>
// - <https://github.com/Genymobile/scrcpy/issues/6523>
if (Context.CLIPBOARD_SERVICE.equals(name) || "semclipboard".equals(name) || Context.ACTIVITY_SERVICE.equals(name)) {
try {
Field field = service.getClass().getDeclaredField("mContext");
field.setAccessible(true);
field.set(service, this);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
return service;
}
}

View File

@@ -414,6 +414,7 @@ public class Options {
if (!value.isEmpty()) {
options.audioEncoder = value;
}
break;
case "power_off_on_close":
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
break;

View File

@@ -24,10 +24,14 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Looper;
import android.system.Os;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
@@ -55,17 +59,7 @@ public final class Server {
this.fatalError = true;
}
if (running == 0 || this.fatalError) {
notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
Looper.getMainLooper().quitSafely();
}
}
}
@@ -172,7 +166,7 @@ public final class Server {
});
}
completion.await();
Looper.loop(); // interrupted by the Completion implementation
} finally {
if (cleanUp != null) {
cleanUp.interrupt();
@@ -201,6 +195,21 @@ public final class Server {
}
}
private static void prepareMainLooper() {
// Like Looper.prepareMainLooper(), but with quitAllowed set to true
Looper.prepare();
synchronized (Looper.class) {
try {
@SuppressLint("DiscouragedPrivateApi")
Field field = Looper.class.getDeclaredField("sMainLooper");
field.setAccessible(true);
field.set(null, Looper.myLooper());
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
}
}
public static void main(String... args) {
int status = 0;
try {
@@ -217,10 +226,18 @@ public final class Server {
}
private static void internalMain(String... args) throws Exception {
Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
if (defaultHandler != null) {
defaultHandler.uncaughtException(t, e);
}
});
dropRootPrivileges();
prepareMainLooper();
Options options = Options.parse(args);
Ln.disableSystemStreams();
@@ -259,4 +276,17 @@ public final class Server {
// Do not print stack trace, a user-friendly error-message has already been logged
}
}
@SuppressWarnings("deprecation")
private static void dropRootPrivileges() {
try {
if (Os.getuid() == 0) {
// Copy-paste does not work with root user
// <https://github.com/Genymobile/scrcpy/issues/6224>
Os.setuid(2000);
}
} catch (Exception e) {
Ln.w("Cannot set UID", e);
}
}
}

View File

@@ -6,9 +6,9 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.app.Instrumentation;
import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes;
import android.media.AudioManager;
@@ -29,8 +29,6 @@ public final class Workarounds {
private static final Object ACTIVITY_THREAD;
static {
prepareMainLooper();
try {
// ActivityThread activityThread = new ActivityThread();
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
@@ -77,19 +75,6 @@ public final class Workarounds {
fillAppContext();
}
@SuppressWarnings("deprecation")
private static void prepareMainLooper() {
// Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
// on a null object reference"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
private static void fillAppInfo() {
try {
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
@@ -118,10 +103,7 @@ public final class Workarounds {
private static void fillAppContext() {
try {
Application app = new Application();
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
Application app = Instrumentation.newApplication(Application.class, FakeContext.get());
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");

View File

@@ -112,8 +112,9 @@ public class ControlMessageReader {
private ControlMessage parseInjectScrollEvent() throws IOException {
Position position = parsePosition();
float hScroll = Binary.i16FixedPointToFloat(dis.readShort());
float vScroll = Binary.i16FixedPointToFloat(dis.readShort());
// Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16].
float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16;
int buttons = dis.readInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons);
}

View File

@@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
@@ -17,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent;
import android.os.Build;
import android.os.SystemClock;
@@ -114,22 +114,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
// Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled)
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardAutosync) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
String text = Device.getClipboardText();
if (text != null) {
DeviceMessage msg = DeviceMessage.createClipboard(text);
sender.send(msg);
}
clipboardManager.addPrimaryClipChangedListener(() -> {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
String text = Device.getClipboardText();
if (text != null) {
DeviceMessage msg = DeviceMessage.createClipboard(text);
sender.send(msg);
}
});
} else {
@@ -156,8 +154,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private UhidManager getUhidManager() {
if (uhidManager == null) {
uhidManager = new UhidManager(sender);
int uhidDisplayId = displayId;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
if (displayId == Device.DISPLAY_ID_NONE) {
// Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be
// associated to the virtual display
try {
// Wait for at most 1 second until a virtual display id is known
DisplayData data = waitDisplayData(1000);
if (data != null) {
uhidDisplayId = data.virtualDisplayId;
}
} catch (InterruptedException e) {
// do nothing
}
}
}
String displayUniqueId = null;
if (uhidDisplayId > 0) {
// Ignore Device.DISPLAY_ID_NONE and 0 (main display)
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId);
if (displayInfo != null) {
displayUniqueId = displayInfo.getUniqueId();
}
}
uhidManager = new UhidManager(sender, displayUniqueId);
}
return uhidManager;
}
@@ -699,7 +723,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (timeout < 0) {
return null;
}
displayDataAvailable.wait(timeout);
if (timeout > 0) {
displayDataAvailable.wait(timeout);
}
data = displayData.get();
}

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import android.os.HandlerThread;
@@ -31,14 +32,20 @@ public final class UhidManager {
private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event)
// Must be unique across the system
private static final String INPUT_PORT = "scrcpy:" + Os.getpid();
private final String displayUniqueId;
private final ArrayMap<Integer, FileDescriptor> fds = new ArrayMap<>();
private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder());
private final DeviceMessageSender sender;
private final MessageQueue queue;
public UhidManager(DeviceMessageSender sender) {
public UhidManager(DeviceMessageSender sender, String displayUniqueId) {
this.sender = sender;
this.displayUniqueId = displayUniqueId;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) {
HandlerThread thread = new HandlerThread("UHidManager");
thread.start();
@@ -52,15 +59,22 @@ public final class UhidManager {
try {
FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0);
try {
// First UHID device added
boolean firstDevice = fds.isEmpty();
FileDescriptor old = fds.put(id, fd);
if (old != null) {
Ln.w("Duplicate UHID id: " + id);
close(old);
}
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc);
String phys = mustUseInputPort() ? INPUT_PORT : null;
byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys);
Os.write(fd, req, 0, req.length);
if (firstDevice) {
addUniqueIdAssociation();
}
registerUhidListener(id, fd);
} catch (Exception e) {
close(fd);
@@ -148,7 +162,7 @@ public final class UhidManager {
}
}
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) {
private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) {
/*
* struct uhid_event {
* uint32_t type;
@@ -170,17 +184,23 @@ public final class UhidManager {
* } __attribute__((__packed__));
*/
byte[] empty = new byte[256];
ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder());
buf.putInt(UHID_CREATE2);
String actualName = name.isEmpty() ? "scrcpy" : name;
byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127);
assert len <= 127;
buf.put(utf8Name, 0, len);
buf.put(empty, 0, 256 - len);
byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8);
int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127);
assert nameLen <= 127;
buf.put(nameBytes, 0, nameLen);
if (phys != null) {
buf.position(4 + 128);
byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII);
assert physBytes.length <= 63;
buf.put(physBytes);
}
buf.position(4 + 256);
buf.putShort((short) reportDesc.length);
buf.putShort(BUS_VIRTUAL);
buf.putInt(vendorId);
@@ -219,15 +239,26 @@ public final class UhidManager {
if (fd != null) {
unregisterUhidListener(fd);
close(fd);
if (fds.isEmpty()) {
// Last UHID device removed
removeUniqueIdAssociation();
}
} else {
Ln.w("Closing unknown UHID device: " + id);
}
}
public void closeAll() {
if (fds.isEmpty()) {
return;
}
for (FileDescriptor fd : fds.values()) {
close(fd);
}
removeUniqueIdAssociation();
}
private static void close(FileDescriptor fd) {
@@ -237,4 +268,20 @@ public final class UhidManager {
Ln.e("Failed to close uhid: " + e.getMessage());
}
}
private boolean mustUseInputPort() {
return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null;
}
private void addUniqueIdAssociation() {
if (mustUseInputPort()) {
ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId);
}
}
private void removeUniqueIdAssociation() {
if (mustUseInputPort()) {
ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT);
}
}
}

View File

@@ -7,16 +7,18 @@ public final class DisplayInfo {
private final int layerStack;
private final int flags;
private final int dpi;
private final String uniqueId;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) {
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) {
this.displayId = displayId;
this.size = size;
this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
this.dpi = dpi;
this.uniqueId = uniqueId;
}
public int getDisplayId() {
@@ -42,5 +44,8 @@ public final class DisplayInfo {
public int getDpi() {
return dpi;
}
}
public String getUniqueId() {
return uniqueId;
}
}

View File

@@ -32,9 +32,11 @@ public enum Orientation {
throw new IllegalArgumentException("Unknown orientation: " + name);
}
public static Orientation fromRotation(int rotation) {
assert rotation >= 0 && rotation < 4;
return values()[rotation];
public static Orientation fromRotation(int ccwRotation) {
assert ccwRotation >= 0 && ccwRotation < 4;
// Display rotation is expressed counter-clockwise, orientation is expressed clockwise
int cwRotation = (4 - ccwRotation) % 4;
return values()[cwRotation];
}
public boolean isFlipped() {

View File

@@ -1,6 +1,7 @@
package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Threads;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
@@ -15,6 +16,7 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.view.Surface;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
public final class OpenGLRunner {
@@ -80,31 +82,17 @@ public final class OpenGLRunner {
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
initOnce();
// Simulate CompletableFuture, but working for all Android versions
final Semaphore sem = new Semaphore(0);
Throwable[] throwableRef = new Throwable[1];
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
// See <https://github.com/Genymobile/scrcpy/issues/5444>
handler.post(() -> {
try {
run(inputSize, outputSize, outputSurface);
} catch (Throwable throwable) {
throwableRef[0] = throwable;
} finally {
sem.release();
}
});
try {
sem.acquire();
} catch (InterruptedException e) {
// Behave as if this method call was synchronous
Thread.currentThread().interrupt();
}
Throwable throwable = throwableRef[0];
if (throwable != null) {
Threads.executeSynchronouslyOn(handler, new Callable<Void>() {
@Override
public Void call() throws Exception {
run(inputSize, outputSize, outputSurface);
return null;
}
});
} catch (Throwable throwable) {
if (throwable instanceof OpenGLException) {
throw (OpenGLException) throwable;
}

View File

@@ -74,9 +74,11 @@ public final class Ln {
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable);
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR);
synchronized (CONSOLE_ERR) {
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR);
}
}
}
}
@@ -88,9 +90,11 @@ public final class Ln {
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR);
synchronized (CONSOLE_ERR) {
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
if (throwable != null) {
throwable.printStackTrace(CONSOLE_ERR);
}
}
}
}

View File

@@ -1,13 +1,8 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public final class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
@@ -18,66 +13,26 @@ public final class Settings {
/* not instantiable */
}
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
try {
Command.exec("settings", "put", table, key, value);
} catch (IOException | InterruptedException e) {
throw new SettingsException("put", table, key, value, e);
}
}
private static String execSettingsGet(String table, String key) throws SettingsException {
try {
return Command.execReadLine("settings", "get", table, key);
} catch (IOException | InterruptedException e) {
throw new SettingsException("get", table, key, null, e);
}
}
public static String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
} catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
}
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
}
return execSettingsGet(table, key);
}
public static void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
} catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
}
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
}
execSettingsPut(table, key, value);
}
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
}
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
}

View File

@@ -0,0 +1,43 @@
package com.genymobile.scrcpy.util;
import android.os.Handler;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
public final class Threads {
private Threads() {
// not instantiable
}
public static <T> T executeSynchronouslyOn(Handler handler, Callable<T> callable) throws Throwable {
// Simulate CompletableFuture, but working for all Android versions
final Semaphore sem = new Semaphore(0);
@SuppressWarnings("unchecked")
T[] resultRef = (T[]) new Object[1];
Throwable[] throwableRef = new Throwable[1];
handler.post(() -> {
try {
resultRef[0] = callable.call();
} catch (Throwable throwable) {
throwableRef[0] = throwable;
} finally {
sem.release();
}
});
try {
sem.acquire();
} catch (InterruptedException e) {
// Behave as if this method call was synchronous
Thread.currentThread().interrupt();
}
if (throwableRef[0] != null) {
throw throwableRef[0];
}
return resultRef[0];
}
}

View File

@@ -25,6 +25,7 @@ public class NewDisplayCapture extends SurfaceCapture {
// Internal fields copied from android.hardware.display.DisplayManager
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
private static final int VIRTUAL_DISPLAY_FLAG_PRESENTATION = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
@@ -169,6 +170,7 @@ public class NewDisplayCapture extends SurfaceCapture {
int virtualDisplayId;
try {
int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
| VIRTUAL_DISPLAY_FLAG_PRESENTATION
| VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT;

View File

@@ -1,270 +1,43 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.Method;
import android.content.Context;
public final class ClipboardManager {
private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
private int getMethodVersion;
private int setMethodVersion;
private int addListenerMethodVersion;
private final android.content.ClipboardManager manager;
static ClipboardManager create() {
IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE);
if (manager == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
return new ClipboardManager(clipboard);
return new ClipboardManager(manager);
}
private ClipboardManager(IInterface manager) {
private ClipboardManager(android.content.ClipboardManager manager) {
this.manager = manager;
}
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
return getPrimaryClipMethod;
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 5;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
getMethodVersion = 6;
}
return getPrimaryClipMethod;
}
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
return setPrimaryClipMethod;
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e1) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e2) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e3) {
// fall-through
}
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
return setPrimaryClipMethod;
}
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
switch (methodVersion) {
case 0:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
case 1:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
case 3:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
case 4:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
case 5:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
}
}
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
case 2:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
default:
// The last boolean parameter is "userOperate"
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
}
}
public CharSequence getText() {
try {
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
ClipData clipData = manager.getPrimaryClip();
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
}
public boolean setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, setMethodVersion, manager, clipData);
return true;
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
ClipData clipData = ClipData.newPlainText(null, text);
manager.setPrimaryClip(clipData);
return true;
}
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener)
throws ReflectiveOperationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
}
}
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
addListenerMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class);
addListenerMethodVersion = 1;
} catch (NoSuchMethodException e2) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class, int.class);
addListenerMethodVersion = 2;
}
}
}
}
return addPrimaryClipChangedListener;
}
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
return true;
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) {
manager.addPrimaryClipChangedListener(listener);
}
}

View File

@@ -46,6 +46,7 @@ public final class DisplayManager {
}
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method getDisplayInfoMethod;
private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod;
@@ -81,7 +82,7 @@ public final class DisplayManager {
int density = Integer.parseInt(m.group(5));
int layerStack = Integer.parseInt(m.group(6));
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null);
}
private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) {
@@ -95,12 +96,12 @@ public final class DisplayManager {
}
private static int parseDisplayFlags(String text) {
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
if (text == null) {
return 0;
}
int flags = 0;
Pattern regex = Pattern.compile("FLAG_[A-Z_]+");
Matcher m = regex.matcher(text);
while (m.find()) {
String flagString = m.group();
@@ -114,9 +115,18 @@ public final class DisplayManager {
return flags;
}
// getDisplayInfo() may be used from both the Controller thread and the video (main) thread
private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException {
if (getDisplayInfoMethod == null) {
getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class);
}
return getDisplayInfoMethod;
}
public DisplayInfo getDisplayInfo(int displayId) {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
Method method = getGetDisplayInfoMethod();
Object displayInfo = method.invoke(manager, displayId);
if (displayInfo == null) {
// fallback when displayInfo is null
return getDisplayInfoFromDumpsysDisplay(displayId);
@@ -129,7 +139,14 @@ public final class DisplayManager {
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi);
String uniqueId;
try {
uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo);
} catch (NoSuchFieldException e) {
// This field might not exist: <https://github.com/Genymobile/scrcpy/issues/6461>
uniqueId = null;
}
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}

View File

@@ -1,10 +1,11 @@
package com.genymobile.scrcpy.wrappers;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.IDisplayWindowListener;
import com.genymobile.scrcpy.util.Ln;
import java.util.List;
import android.content.res.Configuration;
import android.os.Parcel;
import android.os.RemoteException;
import android.view.IDisplayWindowListener;
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
@Override
@@ -23,17 +24,14 @@ public class DisplayWindowListener extends IDisplayWindowListener.Stub {
}
@Override
public void onFixedRotationStarted(int displayId, int newRotation) {
// empty default implementation
}
@Override
public void onFixedRotationFinished(int displayId) {
// empty default implementation
}
@Override
public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {
// empty default implementation
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
try {
return super.onTransact(code, data, reply, flags);
} catch (AbstractMethodError e) {
Ln.v("Ignoring AbstractMethodError: " + e.getMessage());
// Ignore unknown methods, write default response to reply parcel
reply.writeNoException();
return true;
}
}
}

View File

@@ -1,11 +1,15 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.view.InputEvent;
import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
@@ -15,39 +19,28 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final Object manager;
private Method injectInputEventMethod;
private final android.hardware.input.InputManager manager;
private long lastPermissionLogDate;
private static Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
private static Method addUniqueIdAssociationByPortMethod;
private static Method removeUniqueIdAssociationByPortMethod;
static InputManager create() {
try {
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
return new InputManager(im);
} catch (ReflectiveOperationException e) {
throw new AssertionError(e);
}
android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get()
.getSystemService(FakeContext.INPUT_SERVICE);
return new InputManager(manager);
}
private static Class<?> getInputManagerClass() {
try {
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
return Class.forName("android.hardware.input.InputManagerGlobal");
} catch (ClassNotFoundException e) {
return android.hardware.input.InputManager.class;
}
}
private InputManager(Object manager) {
private InputManager(android.hardware.input.InputManager manager) {
this.manager = manager;
}
private Method getInjectInputEventMethod() throws NoSuchMethodException {
private static Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class);
}
return injectInputEventMethod;
}
@@ -57,6 +50,23 @@ public final class InputManager {
Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode);
} catch (ReflectiveOperationException e) {
if (e instanceof InvocationTargetException) {
Throwable cause = e.getCause();
if (cause instanceof SecurityException) {
String message = e.getCause().getMessage();
if (message != null && message.contains("INJECT_EVENTS permission")) {
// Do not flood the console, limit to one permission error log every 3 seconds
long now = System.currentTimeMillis();
if (lastPermissionLogDate <= now - 3000) {
Ln.e(message);
Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device.");
lastPermissionLogDate = now;
}
// Do not print the stack trace
return false;
}
}
}
Ln.e("Could not invoke method", e);
return false;
}
@@ -97,4 +107,40 @@ public final class InputManager {
return false;
}
}
private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
if (addUniqueIdAssociationByPortMethod == null) {
addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
"addUniqueIdAssociationByPort", String.class, String.class);
}
return addUniqueIdAssociationByPortMethod;
}
@TargetApi(AndroidVersions.API_35_ANDROID_15)
public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) {
try {
Method method = getAddUniqueIdAssociationByPortMethod();
method.invoke(manager, inputPort, uniqueId);
} catch (ReflectiveOperationException e) {
Ln.e("Cannot add unique id association by port", e);
}
}
private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException {
if (removeUniqueIdAssociationByPortMethod == null) {
removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod(
"removeUniqueIdAssociationByPort", String.class);
}
return removeUniqueIdAssociationByPortMethod;
}
@TargetApi(AndroidVersions.API_35_ANDROID_15)
public void removeUniqueIdAssociationByPort(String inputPort) {
try {
Method method = getRemoveUniqueIdAssociationByPortMethod();
method.invoke(manager, inputPort);
} catch (ReflectiveOperationException e) {
Ln.e("Cannot remove unique id association by port", e);
}
}
}

View File

@@ -54,7 +54,8 @@ public final class ServiceManager {
return windowManager;
}
public static DisplayManager getDisplayManager() {
// The DisplayManager may be used from both the Controller thread and the video (main) thread
public static synchronized DisplayManager getDisplayManager() {
if (displayManager == null) {
displayManager = DisplayManager.create();
}

View File

@@ -125,7 +125,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0); // 0.0f encoded as i16
dos.writeShort(0x8000); // -1.0f encoded as i16
dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16])
dos.writeInt(1);
byte[] packet = bos.toByteArray();
@@ -139,7 +139,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(0f, event.getHScroll(), 0f);
Assert.assertEquals(-1f, event.getVScroll(), 0f);
Assert.assertEquals(-16f, event.getVScroll(), 0f);
Assert.assertEquals(1, event.getButtons());
Assert.assertEquals(-1, bis.read()); // EOS