Compare commits

..

74 Commits

Author SHA1 Message Date
Romain Vimont
323549e82f Prevent build from falling back to system libs
Ensure that if a file or function is not found, the build does not
attempt to use system libraries. Falling back could result in using
libraries that are incompatible due to wrong versions or features.

PR #6671 <https://github.com/Genymobile/scrcpy/pull/6671>
2026-02-21 12:24:41 +01:00
Romain Vimont
90d11810e3 Set MediaCodec KEY_LATENCY to the minimum value
The encoder must output a frame as soon as one frame is queued.

Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_LATENCY>
Refs #6238 comment <https://github.com/Genymobile/scrcpy/issues/6238#issuecomment-3828402687>
PR #6670 <https://github.com/Genymobile/scrcpy/pull/6670>
2026-02-21 12:21:54 +01:00
Romain Vimont
1f19ec4aec Set MediaCodec KEY_PRIORITY to real-time (0)
Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_PRIORITY>
Refs #6238 comment <https://github.com/Genymobile/scrcpy/issues/6238#issuecomment-3828402687>
PR #6670 <https://github.com/Genymobile/scrcpy/pull/6670>
2026-02-21 12:21:44 +01:00
Romain Vimont
8b0206f7be Keep Windows terminal open on error
If scrcpy is launched by double-clicking scrcpy.exe in Windows Explorer,
automatically set --pause-on-exit=if-error.

Without this, the terminal would close immediately, preventing the user
from seeing the error.

Also remove scrcpy-console.bat, which is now useless.

PR #6667 <https://github.com/Genymobile/scrcpy/pull/6667>
2026-02-21 12:17:31 +01:00
Romain Vimont
f91fa56593 Fix segfault on rotation while recording
The packet sink push_session() callback is optional, but it was called
unconditionnally even when not implemented, leading to a segfault.

Bug introduced by commit 78cba1b7c2.

Fixes #6687 <https://github.com/Genymobile/scrcpy/issues/6687>
2026-02-21 10:45:12 +01:00
Romain Vimont
23710e04c1 Disable audio stream with fatal error Throwable
Only an AudioCaptureException should disable the audio stream without
making scrcpy exit (it is not a fatal error).

A ConfigurationException or any other Throwable must be considered a
fatal error.

                               BEFORE      AFTER
  AudioCaptureException:    non-fatal  non-fatal
  ConfigurationException:       fatal      fatal
  any other Throwable:      non-fatal      fatal

Refs #6600 comment <https://github.com/Genymobile/scrcpy/issues/6600#issuecomment-3744934826>
2026-02-12 20:20:52 +01:00
Romain Vimont
51cbd9a5bc Detect TCP devices provided by mDNS
Their serial is not in the form ip:port, but rather a complex string
containing "adb-tls-connect".

Fixes #6248 <https://github.com/Genymobile/scrcpy/issues/6248>
PR #6665 <https://github.com/Genymobile/scrcpy/pull/6665>
2026-02-12 20:08:46 +01:00
Romain Vimont
82e102e036 Improve adb devices parsing
`adb devices -l` prints one device per line, containing, separated by
spaces:
 - the device serial,
 - the device state,
 - a list of key:value pairs.

However, the device serial itself may contain spaces, making a simple
split ambiguous.

To avoid ambiguity, parse the string backwards:
 - first, parse all the trailing values until the device state,
   identified using a list of well-known values;
 - finally, treat the remaining leading token as the device serial.

Fixes #3537 <https://github.com/Genymobile/scrcpy/issues/3537>
Refs #6248 <https://github.com/Genymobile/scrcpy/issues/6248>
PR #6664 <https://github.com/Genymobile/scrcpy/pull/6664>
2026-02-12 20:06:11 +01:00
Romain Vimont
e39d7a06e4 Remove sc_str_quote()
It is no longer used.

PR #6663 <https://github.com/Genymobile/scrcpy/pull/6663>
2026-02-12 20:00:59 +01:00
Romain Vimont
be34f37a57 Use proper argument serialization for Windows
Replace the hacky implementation for executing simple commands on
Windows with the proper serialization mechanism.

PR #6663 <https://github.com/Genymobile/scrcpy/pull/6663>
2026-02-12 20:00:59 +01:00
Romain Vimont
f7e74dd04d Add utility to serialize windows arguments
Add a function to convert an argv array into a single escaped string to
be passed to CreateProcess() on Windows.

Refs <https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments>
Refs <https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw>
PR #6663 <https://github.com/Genymobile/scrcpy/pull/6663>
2026-02-12 20:00:39 +01:00
Romain Vimont
bb6d1c6348 Set Windows console code page to UTF-8
Refs #6663 comment <https://github.com/Genymobile/scrcpy/pull/6663#issuecomment-3875602219>

Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2026-02-10 08:27:22 +01:00
Romain Vimont
2301f64158 Make icon loading failure non-fatal
Do not close scrcpy if the content icon cannot be loaded, even when
video playback is disabled.
2026-02-09 18:36:54 +01:00
Romain Vimont
d7f0e22067 Make screen event handler return void
The function always returned true.
2026-02-09 18:36:54 +01:00
Romain Vimont
77998f543f Rename sc_display to sc_texture
The sc_display component now only handles a texture. Rendering has been
moved to sc_screen.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:57 +01:00
Romain Vimont
c43cc2ddc3 Set display texture from a frame
Add a function to set a texture from an AVFrame on a sc_display.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:52 +01:00
Romain Vimont
1617979549 Set display texture from a surface
Add a function to set a texture from a surface on a sc_display.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:48 +01:00
Romain Vimont
58818dc860 Use floating-point for content location
In SDL3, texture rendering uses SDL_FRect, unlike SDL2 which used
SDL_Rect.

Compute content location using floating-point coordinates from the
start.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:44 +01:00
Romain Vimont
3ef51e691e Factorize icon rendering
Replace SDL "logical rendering" with explicit computation of icon
location, and unify rendering of video frames and icons using the same
common code.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:41 +01:00
Romain Vimont
fb77f8556a Extract function to compute content location
PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:38 +01:00
Romain Vimont
e3f0c21f2a Use render output size to compute content location
The coordinates of the content to render depend on the render output
size, not the window size in pixels. In theory, the two could differ.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:35 +01:00
Romain Vimont
930169af4e Move rendering from sc_display to sc_screen
Three components are involved in displaying device content on screen:
 - a window
 - a renderer
 - a texture

Originally, all three were handled by sc_screen.

Commit 051b74c883 later extracted the
renderer and texture into a separate component, sc_display.

However, the split was a bit awkward because the window size and
rendering location were managed by separate components.

Move rendering back to sc_screen, keeping only texture management
separated.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:31 +01:00
Romain Vimont
3ce5feb5cb Move renderer from sc_display to sc_screen
Make sc_screen the owner of both the SDL window and the SDL renderer.
This is the first step toward limiting the role of sc_display to texture
management.

PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:29 +01:00
Romain Vimont
42e2264d67 Simplify texture failure handling
When the scrcpy window is minimized on Windows with D3D9, texture
creation and updates fail.

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

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

However, do not close scrcpy when texture creation or updates fail; only
that specific rendering should fail.

Refs SDL/#7651 <https://github.com/libsdl-org/SDL/issues/7651>
Refs #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
Refs 6298ef095f
PR #6651 <https://github.com/Genymobile/scrcpy/pull/6651>
2026-02-09 18:33:18 +01:00
Romain Vimont
c497718a3e Report recording error on write error
The error flag was mistakenly set to false instead of true.
2026-02-05 18:27:05 +01:00
Romain Vimont
07f056353b Use common sc_screen for normal and OTG modes
Originally, the default scrcpy window always displayed the video stream.
Since the OTG mode window only contains the scrcpy logo, it was
implemented as a separate component [1].

Later, the --no-video option was added [2] to control the device without
mirroring (while still using an adb connection, unlike OTG mode). To
support this, sc_screen gained the ability to display a window
containing only the scrcpy logo, like in OTG mode.

As a result, the main sc_screen component can now be reused in OTG mode,
allowing removal of the OTG-specific duplicate implementation
(sc_screen_otg).

[1] commit 91418c79ab
[2] commit 8c650e53cd

Refs #2974 <https://github.com/Genymobile/scrcpy/pull/2974>
Refs #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
PR #6649 <https://github.com/Genymobile/scrcpy/pull/6649>
2026-02-04 20:15:31 +01:00
Romain Vimont
1a99a46b57 Fix switch-case style
Remove unnecessary braces around the switch case and add the missing
break to keep the style consistent with other cases.
2026-02-03 19:12:00 +01:00
Romain Vimont
a9de53f0cb Fix file drop handling with --no-video
The "file pusher" can be used when a window is present, but it was only
initialized when video playback was enabled, causing a segfault on file
drop when running without video playback.
2026-02-02 22:40:23 +01:00
Romain Vimont
c07500bb03 Fix window hiding condition
If a window is present, it must be hidden on quit, regardless of whether
video playback is enabled.
2026-02-02 22:40:23 +01:00
Romain Vimont
373b366fa5 Do not attempt to increase audio thread priority
In practice, this always fails for non-privileged processes.
2026-01-17 21:30:19 +01:00
Romain Vimont
5607f12f2a Document camera torch and zoom features
PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:41:09 +01:00
Tommie
8ac04d39f4 Add shortcuts to change the camera zoom
MOD+up and MOD+zoom change the camera zoom.

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

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

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:37:42 +01:00
Romain Vimont
1018f3e9be Display fps set with '{}'
Replace "fps=[15, 30, 60]" with "fps={15, 30, 60}".

The default toString() implementation for a SortedSet uses '[]', but
it is more correct to use '{}' to denote a set.

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

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

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

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

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

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

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

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

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

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

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

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

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:08:40 +01:00
Romain Vimont
826076f753 Detect frame size mismatch in decoder
Warn if the size of a decoded video frame does not match the session
metadata.

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
c36433999c Properly handle session packets in delay_buffer
The delay buffer must forward the session packets while preserving
their order relative to media packets.

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
78cba1b7c2 Add session metadata for the video stream
Introduce a new packet type, a "session" packet, containing metadata
about the encoding session. It is used only for the video stream and
currently includes the video resolution.

For illustration, here is a sequence of packets on the video stream:

                                        device rotation
                                        v
    CODEC | SESSION | MEDIA | MEDIA | … | SESSION | MEDIA | MEDIA | …
           1920x1080 <-----------------> 1080x1920 <------------------
                      encoding session 1            encoding session 2

This metadata is not strictly necessary, since the video resolution can
be determined after decoding. However, it allows detection of cases
where the encoder does not respect the requested size (and logs a
warning), even without decoding (e.g., when there is no video playback).

Additional metadata could be added later if necessary, for example the
actual device rotation.

Refs #5918 <https://github.com/Genymobile/scrcpy/pull/5918>
Refs #5894 <https://github.com/Genymobile/scrcpy/pull/5894>
PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>

Co-authored-by: gz0119 <liyong2@4399.com>
2026-01-10 11:53:22 +01:00
Romain Vimont
1015b42e53 Rename "codec meta" to "stream meta"
The stream metadata will contain both:
 - the codec ID at the start of the stream
 - the session metadata (video width and height) at the start of each
   "session" (typically on rotation)

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
f4cc07da24 Keep the window hidden during initialization
This prevents blinking during display initialization.
2026-01-09 19:08:04 +01:00
Romain Vimont
fde02a7dfa Fix segfault with --no-video
Do not call SDL_RectToFRect() if geometry is NULL.

Bug introduced by 02989249f6.
2026-01-09 19:06:08 +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
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
62 changed files with 1134 additions and 1335 deletions

View File

@@ -72,13 +72,19 @@ 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
@@ -106,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
@@ -140,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
@@ -171,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
@@ -237,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: |

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.3.3)
# scrcpy (v3.3.4)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@@ -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

View File

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

View File

@@ -3,9 +3,9 @@ set -ex
. $(dirname ${BASH_SOURCE[0]})/_init
process_args "$@"
VERSION=3.2.18
VERSION=3.4.0
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
SHA256SUM=51539fa13e546bc50c632beed3f34257de2baa38a4c642048de56377903b4265
SHA256SUM=9614b9696abc4597ffce6b888829dc6537ae500423474c342ac4a67222c5654c
PROJECT_DIR="sdl-$VERSION"
FILENAME="$PROJECT_DIR.tar.gz"
@@ -37,6 +37,7 @@ else
conf=(
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/$DIRNAME"
-DSDL_TESTS=OFF
)
if [[ "$HOST" == linux ]]

View File

@@ -15,7 +15,6 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/events.c',
'src/icon.c',
'src/file_pusher.c',
@@ -33,6 +32,7 @@ src = [
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/texture.c',
'src/version.c',
'src/hid/hid_gamepad.c',
'src/hid/hid_keyboard.c',
@@ -75,6 +75,7 @@ conf.set('_GNU_SOURCE', true)
if host_machine.system() == 'windows'
windows = import('windows')
src += [
'src/util/command.c',
'src/sys/win/file.c',
'src/sys/win/process.c',
windows.compile_resources('scrcpy-windows.rc'),
@@ -104,7 +105,6 @@ if usb_support
'src/usb/keyboard_aoa.c',
'src/usb/mouse_aoa.c',
'src/usb/scrcpy_otg.c',
'src/usb/screen_otg.c',
'src/usb/usb.c',
]
endif
@@ -239,6 +239,12 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c',
'src/util/term.c',
]],
['test_command_windows', [
'tests/test_command_windows.c',
'src/util/command.c',
'src/util/str.c',
'src/util/strbuf.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
@@ -276,7 +282,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.3.3"
VALUE "ProductVersion", "3.3.4"
END
END
BLOCK "VarFileInfo"

View File

@@ -824,13 +824,21 @@ Install APK from computer
Push file to device (see \fB\-\-push\-target\fR)
.TP
.B MOD+l
.B MOD+t
Turn on the camera torch (camera mode only)
.TP
.B MOD+Shift+l
.B MOD+Shift+t
Turn off the camera torch (camera mode only)
.TP
.B MOD+Up
Zoom camera in (camera mode only)
.TP
.B MOD+Down
Zoom camera out (camera mode only)
.SH Environment variables

View File

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

View File

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

View File

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

View File

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

View File

@@ -1221,13 +1221,21 @@ static const struct sc_shortcut shortcuts[] = {
.text = "Push file to device (see --push-target)",
},
{
.shortcuts = { "MOD+l" },
.shortcuts = { "MOD+t" },
.text = "Turn on the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Shift+l" },
.shortcuts = { "MOD+Shift+t" },
.text = "Turn off the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Up" },
.text = "Zoom camera in (camera mode only)",
},
{
.shortcuts = { "MOD+Down" },
.text = "Zoom camera out (camera mode only)",
},
};
static const struct sc_envvar envvars[] = {
@@ -3363,7 +3371,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
}
if (arg[15] != '=') {
// Invalid parameter, ignore
return SC_PAUSE_ON_EXIT_FALSE;
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
const char *value = &arg[16];
if (!strcmp(value, "true")) {
@@ -3372,14 +3380,44 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
// Set to false, including when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
if (!strcmp(value, "false")) {
return SC_PAUSE_ON_EXIT_FALSE;
}
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
}
return SC_PAUSE_ON_EXIT_FALSE;
return SC_PAUSE_ON_EXIT_UNDEFINED;
}
#ifdef _WIN32
/**
* Attempt to detect whether the user launched scrcpy by double-clicking
* scrcpy.exe in Windows Explorer.
*
* If so, the console should remain open on error.
*/
static bool
scrcpy_launched_by_double_click(void) {
// No console window
if (GetConsoleWindow() == NULL) {
return false;
}
// Must be interactive
if (!_isatty(_fileno(stdin)) || !_isatty(_fileno(stdout))) {
return false;
}
// Check how many processes share the console
DWORD dummy;
DWORD count = GetConsoleProcessList(&dummy, 1);
// Only this process attached, assume it was started by double-clicking
return count == 1;
}
#endif
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
struct sc_getopt_adapter adapter;
@@ -3393,11 +3431,22 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
sc_getopt_adapter_destroy(&adapter);
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) {
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
// Check if "--pause-on-exit" is present in the arguments list, because
// it must be taken into account even if command line parsing failed
args->pause_on_exit = sc_get_pause_on_exit(argc, argv);
}
if (args->pause_on_exit == SC_PAUSE_ON_EXIT_UNDEFINED) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_FALSE;
#ifdef _WIN32
if (scrcpy_launched_by_double_click()) {
args->pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR;
}
#endif
}
assert(args->pause_on_exit != SC_PAUSE_ON_EXIT_UNDEFINED);
return ret;
}

View File

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

View File

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

View File

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

View File

@@ -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

@@ -261,7 +261,7 @@ load_from_path(const char *path) {
// frame owns the data
bool ok = SDL_SetPointerProperty(props, "sc_frame", frame);
if (!ok) {
LOGE("Could not get surface properties: %s", SDL_GetError());
LOGE("Could not set pointer property: %s", SDL_GetError());
SDL_DestroySurface(surface);
goto error;
}

View File

@@ -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
@@ -322,9 +323,6 @@ enum sc_mouse_button {
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_GAMEPAD_AXIS_LEFTX,
@@ -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;
@@ -500,7 +497,7 @@ static inline enum sc_gamepad_axis
sc_gamepad_axis_from_sdl(uint8_t axis) {
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;
@@ -510,14 +507,14 @@ static inline enum sc_gamepad_button
sc_gamepad_button_from_sdl(uint8_t button) {
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) {
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;

View File

@@ -16,8 +16,6 @@
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
// A key/mouse processor may not be present if there is no controller
assert((!params->kp && !params->mp && !params->gp) || params->controller);
// A processor must have ops initialized
assert(!params->kp || params->kp->ops);
assert(!params->mp || params->mp->ops);
@@ -620,7 +618,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
if (control && im->camera) {
switch (sdl_keycode) {
case SDLK_L:
case SDLK_T:
if (!repeat && down) {
camera_set_torch(im, !shift);
}
@@ -892,7 +890,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
SDL_FRect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
@@ -1020,16 +1018,16 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
}
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
if (!gc) {
LOGW("Could not open game controller");
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
return;
}
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (!joystick) {
LOGW("Could not get controller joystick");
SDL_CloseGamepad(gc);
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
return;
}
@@ -1040,9 +1038,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
if (gc) {
SDL_CloseGamepad(gc);
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
} else {
LOGW("Unknown gamepad device removed");
}
@@ -1091,7 +1089,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);
@@ -1166,8 +1164,8 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
case SDL_EVENT_GAMEPAD_BUTTON_UP:
sc_input_manager_process_gamepad_button(im, &event->gbutton);
break;
case SDL_EVENT_DROP_FILE: {
case SDL_EVENT_DROP_FILE:
sc_input_manager_process_file(im, &event->drop);
}
break;
}
}

View File

@@ -5,14 +5,14 @@
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
#include <SDL3/SDL_stdinc.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"
@@ -39,7 +39,7 @@ main_scrcpy(int argc, char *argv[]) {
.opts = scrcpy_options_default,
.help = false,
.version = false,
.pause_on_exit = SC_PAUSE_ON_EXIT_FALSE,
.pause_on_exit = SC_PAUSE_ON_EXIT_UNDEFINED,
};
#ifndef NDEBUG

View File

@@ -57,8 +57,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
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;

View File

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

View File

@@ -199,8 +199,8 @@ event_loop(struct scrcpy *s, bool has_screen) {
break;
}
default:
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
if (has_screen) {
sc_screen_handle_event(&s->screen, &event);
}
break;
}
@@ -355,7 +355,7 @@ 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 count;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&count);
@@ -564,7 +564,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
if (options->video_playback && options->control) {
if (options->window && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
@@ -949,7 +949,7 @@ aoa_complete:
terminate_event_loop();
LOGD("quit...");
if (options->video_playback) {
if (options->window) {
// Close the window immediately on closing, because screen_destroy()
// may only be called once the video demuxer thread is joined (it may
// take time)

View File

@@ -27,13 +27,11 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
return oriented_size;
}
// set the window size to be applied when fullscreen is disabled
static inline void
assert_not_fullscreen(struct sc_screen *screen) {
(void) screen;
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
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)
@@ -41,6 +39,11 @@ static bool
get_preferred_display_bounds(struct sc_size *bounds) {
SDL_Rect 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());
@@ -141,64 +144,110 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
SDL_Rect *rect = &screen->rect;
if (is_optimal_size(drawable_size, content_size)) {
compute_content_rect(struct sc_size render_size, struct sc_size content_size,
bool can_upscale, SDL_FRect *rect) {
if (is_optimal_size(render_size, content_size)) {
rect->x = 0;
rect->y = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
rect->w = render_size.width;
rect->h = render_size.height;
return;
}
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
if (!can_upscale && content_size.width <= render_size.width
&& content_size.height <= render_size.height) {
// Center without upscaling
rect->x = (render_size.width - content_size.width) / 2.f;
rect->y = (render_size.height - content_size.height) / 2.f;
rect->w = content_size.width;
rect->h = content_size.height;
return;
}
bool keep_width = content_size.width * render_size.height
> content_size.height * render_size.width;
if (keep_width) {
rect->x = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.width * content_size.height
/ content_size.width;
rect->y = (drawable_size.height - rect->h) / 2;
rect->w = render_size.width;
rect->h = (float) render_size.width * content_size.height
/ content_size.width;
rect->y = (render_size.height - rect->h) / 2.f;
} else {
rect->y = 0;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
rect->h = render_size.height;
rect->w = (float) render_size.height * content_size.width
/ content_size.height;
rect->x = (render_size.width - rect->w) / 2.f;
}
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
// Only upscale video frames, not icon
bool can_upscale = screen->video;
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
compute_content_rect(render_size, screen->content_size, can_upscale,
&screen->rect);
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->video);
assert(screen->has_video_window);
assert(!screen->video || screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation);
(void) res; // any error already logged
}
SDL_Renderer *renderer = screen->renderer;
sc_sdl_render_clear(renderer);
static void
sc_screen_render_novideo(struct sc_screen *screen) {
enum sc_display_result res =
sc_display_render(&screen->display, NULL, SC_ORIENTATION_0);
(void) res; // any error already logged
bool ok = false;
SDL_Texture *texture = screen->tex.texture;
if (!texture) {
LOGW("No texture to render");
goto end;
}
SDL_FRect *geometry = &screen->rect;
enum sc_orientation orientation = screen->orientation;
if (orientation == SC_ORIENTATION_0) {
ok = SDL_RenderTexture(renderer, texture, NULL, geometry);
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
const SDL_FRect *dstrect = NULL;
SDL_FRect rect;
if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
rect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
dstrect = geometry;
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
ok = SDL_RenderTextureRotated(renderer, texture, NULL, dstrect, angle,
NULL, flip);
}
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
}
end:
sc_sdl_render_present(renderer);
}
#if defined(__APPLE__) || defined(_WIN32)
@@ -216,7 +265,7 @@ event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data;
assert(screen->video);
if (event->type == SDL_EVENT_WINDOW_EXPOSED) {
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);
@@ -294,9 +343,6 @@ sc_screen_init(struct sc_screen *screen,
screen->resize_pending = false;
screen->has_frame = false;
screen->has_video_window = false;
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@@ -328,7 +374,8 @@ sc_screen_init(struct sc_screen *screen,
}
}
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
// Always create the window hidden to prevent blinking during initialization
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
@@ -337,8 +384,7 @@ sc_screen_init(struct sc_screen *screen,
}
if (params->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE;
window_flags |= SDL_WINDOW_RESIZABLE;
}
const char *title = params->window_title;
@@ -369,10 +415,46 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
screen->gl_context = NULL;
// starts with "opengl"
const char *renderer_name = SDL_GetRendererName(screen->renderer);
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
screen->gl_context = SDL_GL_CreateContext(screen->window);
if (!screen->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
goto error_destroy_renderer;
}
}
#endif
bool mipmaps = params->video;
ok = sc_texture_init(&screen->tex, screen->renderer, mipmaps);
if (!ok) {
goto error_destroy_renderer;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
goto error_destroy_window;
goto error_destroy_texture;
}
SDL_Surface *icon = scrcpy_icon_load();
@@ -380,30 +462,32 @@ sc_screen_init(struct sc_screen *screen,
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_window;
}
SDL_Surface *icon_novideo = params->video ? NULL : icon;
bool mipmaps = params->video && params->mipmaps;
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
mipmaps);
if (icon) {
if (!params->video) {
screen->content_size.width = icon->w;
screen->content_size.height = icon->h;
ok = sc_texture_set_from_surface(&screen->tex, icon);
if (!ok) {
LOGE("Could not set icon: %s", SDL_GetError());
}
}
scrcpy_icon_destroy(icon);
}
if (!ok) {
goto error_destroy_window;
} else {
// not fatal
LOGE("Could not load icon");
if (!params->video) {
// Make sure the content size is initialized
screen->content_size.width = 256;
screen->content_size.height = 256;
}
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_display;
goto error_destroy_texture;
}
struct sc_input_manager_params im_params = {
@@ -447,15 +531,27 @@ sc_screen_init(struct sc_screen *screen,
screen->open = false;
#endif
if (!screen->video && sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
if (!screen->video) {
// Show the window immediately
sc_sdl_show_window(screen->window);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
}
}
return true;
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_texture:
sc_texture_destroy(&screen->tex);
error_destroy_renderer:
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
if (screen->gl_context) {
SDL_GL_DestroyContext(screen->gl_context);
}
#endif
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -481,7 +577,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
get_initial_optimal_size(screen->content_size, screen->req.width,
screen->req.height);
assert_not_fullscreen(screen);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, window_size);
sc_sdl_set_window_position(screen->window, position);
@@ -517,8 +613,12 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_display_destroy(&screen->display);
sc_texture_destroy(&screen->tex);
av_frame_free(&screen->frame);
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(screen->gl_context);
#endif
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
@@ -537,7 +637,7 @@ 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);
assert_not_fullscreen(screen);
assert(is_windowed(screen));
sc_sdl_set_window_size(screen->window, target_size);
}
@@ -545,7 +645,7 @@ 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
@@ -561,9 +661,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,6 +701,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
if (!screen->has_frame
|| screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
screen->frame_size = new_frame_size;
@@ -616,28 +715,12 @@ sc_screen_apply_frame(struct sc_screen *screen) {
screen->has_frame = true;
screen->content_size = new_content_size;
}
enum sc_display_result res =
sc_display_prepare_texture(&screen->display, screen->frame_size,
frame->colorspace, frame->color_range);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
}
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
assert(screen->has_frame);
if (!screen->has_video_window) {
@@ -693,7 +776,10 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
sc_screen_apply_frame(screen);
bool ok = sc_screen_apply_frame(screen);
if (!ok) {
LOGE("Resume frame update failed");
}
}
if (!paused) {
@@ -711,26 +797,23 @@ void
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
assert(screen->video);
bool ok = SDL_SetWindowFullscreen(screen->window, !screen->fullscreen);
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;
}
@@ -759,22 +842,17 @@ 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) {
sc_sdl_restore_window(screen->window);
screen->maximized = false;
}
struct sc_size content_size = screen->content_size;
sc_sdl_set_window_size(screen->window, content_size);
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
content_size.height);
}
bool
void
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
// !video implies !has_video_window
assert(screen->video || !screen->has_video_window);
@@ -783,54 +861,46 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
return false;
}
return true;
return;
}
case SDL_EVENT_WINDOW_EXPOSED:
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
if (!screen->video || screen->has_video_window) {
sc_screen_render(screen, true);
}
return true;
return;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return true;
case SDL_EVENT_WINDOW_MAXIMIZED:
screen->maximized = true;
return true;
case SDL_EVENT_WINDOW_MINIMIZED:
screen->minimized = true;
return true;
return;
case SDL_EVENT_WINDOW_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).
return true;
}
screen->maximized = false;
screen->minimized = false;
if (screen->has_video_window) {
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return true;
return;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->has_video_window);
return;
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;
}
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return true;
return;
}
sc_input_manager_handle_event(&screen->im, event);
return true;
}
struct sc_point

View File

@@ -12,16 +12,20 @@
#include "controller.h"
#include "coords.h"
#include "display.h"
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
#include "mouse_capture.h"
#include "options.h"
#include "texture.h"
#include "trait/key_processor.h"
#include "trait/frame_sink.h"
#include "trait/mouse_processor.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_screen {
struct sc_frame_sink frame_sink; // frame sink trait
@@ -32,7 +36,7 @@ struct sc_screen {
bool video;
bool camera;
struct sc_display display;
struct sc_texture tex;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
@@ -49,6 +53,11 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@@ -60,12 +69,9 @@ struct sc_screen {
// client orientation
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_Rect rect;
struct SDL_FRect rect;
bool has_frame;
bool has_video_window;
bool fullscreen;
bool maximized;
bool minimized;
AVFrame *frame;
@@ -151,8 +157,7 @@ void
sc_screen_set_paused(struct sc_screen *screen, bool paused);
// react to SDL events
// If this function returns false, scrcpy must exit with an error.
bool
void
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
// convert point from window coordinates to frame coordinates

View File

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

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

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

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

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

View File

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

View File

@@ -74,10 +74,9 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
return;
}
SDL_Gamepad * game_controller =
SDL_GetGamepadFromID(event->gamepad_id);
assert(game_controller);
const char *name = SDL_GetGamepadName(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

@@ -9,7 +9,7 @@
# include "adb/adb.h"
#endif
#include "events.h"
#include "usb/screen_otg.h"
#include "screen.h"
#include "usb/aoa_hid.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
@@ -23,7 +23,7 @@ struct scrcpy_otg {
struct sc_mouse_aoa mouse;
struct sc_gamepad_aoa gamepad;
struct sc_screen_otg screen_otg;
struct sc_screen screen;
};
static void
@@ -49,7 +49,7 @@ event_loop(struct scrcpy_otg *s) {
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
default:
sc_screen_otg_handle_event(&s->screen_otg, &event);
sc_screen_handle_event(&s->screen, &event);
break;
}
}
@@ -75,7 +75,7 @@ scrcpy_otg(struct scrcpy_options *options) {
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
// Not fatal, keyboard/mouse should still work
}
}
@@ -88,13 +88,14 @@ scrcpy_otg(struct scrcpy_options *options) {
enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE;
struct sc_keyboard_aoa *keyboard = NULL;
struct sc_mouse_aoa *mouse = NULL;
struct sc_gamepad_aoa *gamepad = NULL;
struct sc_key_processor *kp = NULL;
struct sc_mouse_processor *mp = NULL;
struct sc_gamepad_processor *gp = NULL;
bool usb_device_initialized = false;
bool usb_connected = false;
bool aoa_started = false;
bool aoa_initialized = false;
bool screen_initialized = false;
#ifdef _WIN32
// On Windows, only one process could open a USB device
@@ -157,7 +158,7 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
keyboard = &s->keyboard;
kp = &s->keyboard.key_processor;
}
if (enable_mouse) {
@@ -165,12 +166,12 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
mouse = &s->mouse;
mp = &s->mouse.mouse_processor;
}
if (enable_gamepad) {
sc_gamepad_aoa_init(&s->gamepad, &s->aoa);
gamepad = &s->gamepad;
gp = &s->gamepad.gamepad_processor;
}
ok = sc_aoa_start(&s->aoa);
@@ -184,10 +185,18 @@ scrcpy_otg(struct scrcpy_options *options) {
window_title = usb_device.product ? usb_device.product : "scrcpy";
}
struct sc_screen_otg_params params = {
.keyboard = keyboard,
.mouse = mouse,
.gamepad = gamepad,
struct sc_screen_params params = {
.video = false,
.camera = false,
.controller = false,
.fp = NULL,
.kp = kp,
.mp = mp,
.gp = gp,
.mouse_bindings = options->mouse_bindings,
.legacy_paste = false,
.clipboard_autosync = false,
.shortcut_mods = options->shortcut_mods,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@@ -195,13 +204,17 @@ scrcpy_otg(struct scrcpy_options *options) {
.window_width = options->window_width,
.window_height = options->window_height,
.window_borderless = options->window_borderless,
.shortcut_mods = options->shortcut_mods,
.orientation = SC_ORIENTATION_0,
.mipmaps = options->mipmaps,
.fullscreen = false,
.start_fps_counter = false,
};
ok = sc_screen_otg_init(&s->screen_otg, &params);
ok = sc_screen_init(&s->screen, &params);
if (!ok) {
goto end;
}
screen_initialized = true;
// usb_device not needed anymore
sc_usb_device_destroy(&usb_device);
@@ -216,13 +229,17 @@ end:
}
sc_usb_stop(&s->usb);
if (mouse) {
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
}
if (mp) {
sc_mouse_aoa_destroy(&s->mouse);
}
if (keyboard) {
if (kp) {
sc_keyboard_aoa_destroy(&s->keyboard);
}
if (gamepad) {
if (gp) {
sc_gamepad_aoa_destroy(&s->gamepad);
}
@@ -243,5 +260,10 @@ end:
sc_usb_destroy(&s->usb);
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
}
return ret;
}

View File

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

View File

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

98
app/src/util/command.c Normal file
View File

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

17
app/src/util/command.h Normal file
View File

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

View File

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

View File

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

View File

@@ -16,12 +16,12 @@ sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
struct sc_size
sc_sdl_get_window_size(SDL_Window *window);
void
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size);
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);
@@ -34,8 +34,8 @@ sc_sdl_show_window(SDL_Window *window);
void
sc_sdl_hide_window(SDL_Window *window);
void
sc_sdl_restore_window(SDL_Window *window);
struct sc_size
sc_sdl_get_render_output_size(SDL_Renderer *renderer);
bool
sc_sdl_render_clear(SDL_Renderer *renderer);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.3.3`][direct-scrcpy-server]
<sub>SHA-256: `7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0`</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.3.3/scrcpy-server-v3.3.3
[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

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

View File

@@ -6,11 +6,11 @@
Download a static build of the [latest release]:
- [`scrcpy-linux-x86_64-v3.3.3.tar.gz`][direct-linux-x86_64] (x86_64)
<sub>SHA-256: `9b30e813e8191329ba8025dc80cb0f198fb0a318960a3b5c15395cf675c9c638`</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.3.3/scrcpy-linux-x86_64-v3.3.3.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.3.3.tar.gz`][direct-macos-aarch64] (aarch64)
<sub>SHA-256: `b93299468f19ae89ac70f7c1453914c41f1f2bcd31f6ab530038da885c19581f`</sub>
- [`scrcpy-macos-x86_64-v3.3.3.tar.gz`][direct-macos-x86_64] (x86_64)
<sub>SHA-256: `c767fc1d41e4ae26e40558656570962f474739924fd22ee023d8754889ee4366`</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.3.3/scrcpy-macos-aarch64-v3.3.3.tar.gz
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-x86_64-v3.3.3.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

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

View File

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

View File

@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-server-v3.3.3
PREBUILT_SERVER_SHA256=7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0
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.3.3',
version: '3.3.4',
meson_version: '>= 0.49',
default_options: [
'c_std=c11',

View File

@@ -22,9 +22,12 @@ app/deps/libusb.sh linux native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$LINUX_BUILD_DIR"
meson setup "$LINUX_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \

View File

@@ -22,9 +22,12 @@ app/deps/libusb.sh macos native static
DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static"
ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos"
# Never fall back to system libs
unset PKG_CONFIG_PATH
export PKG_CONFIG_LIBDIR="$DEPS_INSTALL_DIR/lib/pkgconfig"
rm -rf "$MACOS_BUILD_DIR"
meson setup "$MACOS_BUILD_DIR" \
--pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \
-Dc_args="-I$DEPS_INSTALL_DIR/include" \
-Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \
--buildtype=release \

View File

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

View File

@@ -7,8 +7,8 @@ android {
applicationId = "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 36
versionCode 30303
versionName "3.3.3"
versionCode 30304
versionName "3.3.4"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=3.3.3
SCRCPY_VERSION_NAME=3.3.4
PLATFORM=${ANDROID_PLATFORM:-36}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}

View File

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

View File

@@ -27,6 +27,7 @@ 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;
@@ -233,6 +234,8 @@ public final class Server {
}
});
dropRootPrivileges();
prepareMainLooper();
Options options = Options.parse(args);
@@ -273,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

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

View File

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

View File

@@ -264,6 +264,10 @@ public class SurfaceEncoder implements AsyncProcessor {
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
// real-time priority
format.setInteger(MediaFormat.KEY_PRIORITY, 0);
// output 1 frame as soon as 1 frame is queued
format.setInteger(MediaFormat.KEY_LATENCY, 1);
if (maxFps > 0) {
// The key existed privately before Android 10:
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>

View File

@@ -457,7 +457,7 @@ public class ControlMessageReaderTest {
}
@Test
public void testParseCameraZoomIn() throws IOException {
public void testParseCameraZoomOut() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_OUT);