Compare commits

...

172 Commits

Author SHA1 Message Date
aea6a371aa Remove scrcpy wrapper script for static builds
All portable builds now use the files located in the same directory as
the scrcpy executable by default.

PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>
2024-12-02 18:23:18 +01:00
dc6c279b1e Log adb executable path
Log the ADB executable path (at the DEBUG level) if it is not the
default one.

PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>
2024-12-02 18:23:18 +01:00
6d0ac3626d Use local adb in portable builds
For non-Windows portable builds, use the absolute path to the adb
executable located in the same directory as scrcpy.

On Windows, just use "adb", which is sufficient to use the local one.

PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>
2024-12-02 18:23:18 +01:00
beee42fb06 Load ADB value using sc_get_env()
Contrary to getenv(), the result of sc_get_env() is encoded in UTF-8 on
all platforms. Since it is allocated, it requires an explicit init() and
destroy() functions.

PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>
2024-12-02 18:23:18 +01:00
131372d2c4 Expose sc_get_env() to read environment variable
Contrary to getenv(), sc_get_env() returns an allocated string that is
guaranteed to be encoded in UTF-8 on all platforms (it uses _wgetenv()
internally on Windows and converts the strings).

PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>
2024-12-02 18:23:18 +01:00
0fd7534bd5 Add method to get executable path on MacOS
PR #5560 <https://github.com/Genymobile/scrcpy/pull/5560>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-12-02 18:23:18 +01:00
36574d2ee7 Fix .tar.gz compression
The generated .tar.gz releases were in fact non-gzipped tarballs.

Fixes #5581 <https://github.com/Genymobile/scrcpy/issues/5581>
2024-12-02 08:54:57 +01:00
3b2b3625e4 Accept positional control events without display
The position of touch and scroll must normally be "resolved" with a
"position mapper" associated to the display.

But to support the injection of such events with scrcpy-server alone
without video, handle the case where there is no display.

Fixes #5542 <https://github.com/Genymobile/scrcpy/issues/5542>
2024-12-01 17:22:47 +01:00
b2cdaa4bdc Factorize position mapper resolution
The code was duplicated for touch and scroll events. Extract it to a
private function.

Refs #5542 <https://github.com/Genymobile/scrcpy/issues/5542>
2024-12-01 17:22:47 +01:00
d01373c03c Enable close-on-interrupt for macOS
This behavior is also necessary on macOS.

Fixes #5536 <https://github.com/Genymobile/scrcpy/issues/5536>
2024-11-28 21:02:51 +01:00
ff06b6dcc1 Split network macro conditions
On Windows, interrupting a socket with shutdown() does not wake up
accept() or read() calls, the socket must be closed.

Introduce a new macro constant SC_SOCKET_CLOSE_ON_INTERRUPT, distinct of
_WIN32, because Windows will not be the only platform exhibiting this
behavior.

Refs #5536 <https://github.com/Genymobile/scrcpy/issues/5536>
2024-11-28 21:02:31 +01:00
017a3672a4 Check GitHub runner architecture
Make sure that the releases are built for the expected target arch.
2024-11-28 20:09:21 +01:00
c1351b250e Build macOS x86_64 release
Add actions to build a release for macOS x86_64 in addition to the
aarch64 version.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-28 20:09:21 +01:00
618a978f5b Specify architecture for Linux and macOS releases
PR #5526 <https://github.com/Genymobile/scrcpy/pull/5526>

Co-authored-by: Genxster1998 <ck.2229.ck@gmail.com>
2024-11-28 20:09:18 +01:00
acddd811bf Rename TARGET to TARGET_DIRNAME
This avoids confusion with "$1", which is also documented as "<target>".

If "$1" (the target) is "linux", then TARGET_DIRNAME is
"scrcpy-linux-v3.0".
2024-11-28 19:59:06 +01:00
ee9f7126ff Use FORMAT variable name in package_client.sh
The format is used several times, avoid using "$2" directly.
2024-11-28 19:58:41 +01:00
a18ed1ee7a Simplify GitHub actions step descriptions
Each step is executed within the context of an action, so mentioning the
name of the action is unnecessary.
2024-11-28 19:58:08 +01:00
678025b316 Remove apt update on GitHub Actions
Assume the image is up-to-date.
2024-11-28 19:40:47 +01:00
3e689020ba Fix null return value in DisplayManager.toString()
Ensure DisplayListener.toString() returns a non-null value to prevent a
NullPointerException on certain devices.

Fixes #5537 <https://github.com/Genymobile/scrcpy/issues/5537>
2024-11-27 07:45:35 +01:00
3d1f036c04 Rollback to old --turn-screen-off for Android 15
When the screen is turned off with the new display power method
introduced in Android 15, video mirroring freezes.

Use the Android 14 method for Android 15.

Refs 58ba00fa06
Refs #5418 <https://github.com/Genymobile/scrcpy/pull/5418>
Fixes #5530 <https://github.com/Genymobile/scrcpy/issues/5530>
2024-11-26 15:55:16 +01:00
3d5294c1e5 Set main display power for virtual display
Change the display power of the main display when mirroring a virtual
display, to make it possible to turn off the screen.

Fixes #5522 <https://github.com/Genymobile/scrcpy/issues/5522>
Refs #5530 <https://github.com/Genymobile/scrcpy/issues/5530>
2024-11-26 15:43:41 +01:00
1d2f16dbb5 Fix documentation about default mouse mode
When video playback is turned off, the default mouse mode has changed
from "uhid" to "disabled" in 2c25fd7a80.

Update the documentation accordingly.

Refs #5410 <https://github.com/Genymobile/scrcpy/issues/5410>
Refs #5542 <https://github.com/Genymobile/scrcpy/issues/5542>
2024-11-26 14:10:11 +01:00
7fef051976 Add BlueSky link
Scrcpy now has a BlueSky account.
2024-11-25 20:06:32 +01:00
da8ade88fd Fix link to virtual display doc in README
PR #5525 <https://github.com/Genymobile/scrcpy/pull/5525>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-25 08:21:06 +01:00
74aecc00b5 Update links to 3.0 2024-11-24 18:30:01 +01:00
5e05f2a25b Bump version to 3.0 2024-11-24 17:52:54 +01:00
3d478d7d5b Build FFmpeg with v4l2 support for Linux
So that --v4l2-sink works with Linux static builds.
2024-11-24 17:52:53 +01:00
54e1f8e060 Include scrcpy manpage in Linux and macOS releases 2024-11-24 16:50:47 +01:00
d40224f299 Fix alphabetic order of cli args 2024-11-24 16:37:32 +01:00
0628ffcb0b Merge branch 'master' into release 2024-11-24 16:01:05 +01:00
6f9520f3e2 Test build_without_gradle.sh in GitHub Actions
Build the server without gradle to make sure that the script works.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:46:37 +01:00
a7efb180b9 Add script to release macOS static binary
Provide a prebuilt binary for macOS.

Fixes #1733 <https://github.com/Genymobile/scrcpy/issues/1733>
Fixes #3235 <https://github.com/Genymobile/scrcpy/issues/3235>
Fixes #4489 <https://github.com/Genymobile/scrcpy/issues/4489>
Fixes #5327 <https://github.com/Genymobile/scrcpy/issues/5327>
PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>

Co-authored-by: Muvaffak Onus <me@muvaf.com>
2024-11-24 15:46:23 +01:00
28c372e838 Use generic command for SHA-256
The command sha256sum does not exist on macOS, but `shasum -a256` works
both on Linux and macOS.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-24 15:41:13 +01:00
cb19686d79 Add script to release Linux static binary
Provide a prebuilt binary for Linux.

Fixes #5327 <https://github.com/Genymobile/scrcpy/issues/5327>
PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
93da693e8c Add support for .tar.gz packaging
Make package_client.sh accept an archive format.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
179c664e2b Add static build option
Use static dependencies if the option is set.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
360936248c Add support for build and link types for deps
Make dependencies build scripts more flexible, to accept a build type
(native or cross) and a link type (static or shared).

This lays the groundwork for building binaries for Linux and macOS.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
98d2065d6d Make the ADB dependency script Windows-specific
This will allow adding similar scripts for other platforms.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
6a81fc438b Extract args processing in deps scripts
Extract the code that processes arguments into a function.

This will make it optional, so the script that only downloads the
official ADB binaries will not use arguments.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
cf0098abf0 Store dependencies configure args in bash arrays
This will make it easy to conditionally add items.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
73b595c806 Disable VDPAU and VAAPI for FFmpeg build
They are not used, and this prevents Linux builds from working if the
dependencies are unavailable.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
d74f564f56 Reorder FFmpeg configure args
All --disable, then all --enable.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
7fc6943284 Preserve file permissions in GitHub Actions
The upload-artifact action does not preserve file permissions:
<https://github.com/actions/upload-artifact?#permission-loss>

Even if it is not critical for Windows releases, it will be for other
platforms. Wrap everything in a tarball to keep original permissions.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
a57180047c Split packaging for each target on CI
Create separate jobs for packaging win32 and win64 releases.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
5df218d8f9 Test scrcpy-server in a separate CI job
Use a separate GitHub Action job to build and test the server.

PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:41:13 +01:00
26bf209617 Replace release.mk by release scripts
Since commit 2687d20280, the Makefile
named release.mk stopped handling dependencies between recipes, because
they have to be executed separately (from different Github Actions
jobs).

Using a Makefile no longer provides any real benefit. Replace it by
several individual release scripts for simplicity and readability.

Refs #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
PR #5515 <https://github.com/Genymobile/scrcpy/pull/5515>
2024-11-24 15:40:34 +01:00
dc82425769 Add debugging method for Android >= 11
Fixes #5346 <https://github.com/Genymobile/scrcpy/issues/5346>
PR #5466 <https://github.com/Genymobile/scrcpy/pull/5466>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-24 12:24:32 +01:00
9f39a5f2d6 Determine debugger command at runtime
When server_debugger is enabled, retrieve the device SDK version to
execute the correct command.

PR #5466 <https://github.com/Genymobile/scrcpy/pull/5466>
2024-11-22 11:04:32 +01:00
24588cb637 Add missing aidl in build_without_gradle.sh
Refs 39d51ff2cc
Fixes #5512 <https://github.com/Genymobile/scrcpy/issues/5512>
2024-11-22 07:48:48 +01:00
0e50d1e7db Extract PLATFORM_TOOLS in build_without_gradle.sh
Refs #5512 <https://github.com/Genymobile/scrcpy/issues/5512>
2024-11-22 07:47:24 +01:00
264110fd70 Dissociate virtual display size and capture size
Allow capturing virtual displays at a lower resolution using
-m/--max-size.

In the original implementation in #5370, the virtual display size was
necessarily the same as the capture size. The --max-size value was only
allowed to determine the virtual display size when no explicit size was
provided.

Since the dpi was scaled down accordingly, it is often better to create
a virtual display at the target capture size directly. However, not
everything is rendered according to the virtual display DPI. For
example, a page in Firefox is rendered too big on small virtual
displays. Thus, it makes sense to be able create a virtual display at a
given size, and capture it at a lower resolution with --max-size. This
is now possible using OpenGL filters.

Therefore, change the behavior of --max-size for virtual displays:
 - --max-size does not impact --new-display without size argument
   anymore (the virtual display size is the main display size);
 - it is used to limit the capture size (whether an explicit size is
   provided or not).

This new behavior is consistent with main display capture.

Refs #5370 comment <https://github.com/Genymobile/scrcpy/pull/5370#issuecomment-2438944401>
Refs #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
PR #5506 <https://github.com/Genymobile/scrcpy/pull/5506>
2024-11-21 18:36:23 +01:00
4608a19a13 Upgrade platform-tools (35.0.2) for Windows
Since 35.0.1, the filename has changed on the server from -windows.zip
to -win.zip

The links are referenced from this file:
<https://dl.google.com/android/repository/repository2-2.xml>

Refs <https://www.reddit.com/r/Android/comments/1fhbs7w/download_links_to_platformtoolsadb/>
2024-11-20 08:14:04 +01:00
f1f2711626 Document missing --cask option for macOS
Installing android-platform-tools via brew install requires the option
--cask.

Refs #2004 <https://github.com/Genymobile/scrcpy/pull/2004>
Refs #2231 <https://github.com/Genymobile/scrcpy/pull/2231>
PR #5398 <https://github.com/Genymobile/scrcpy/pull/5398>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-20 08:04:47 +01:00
eeb04292a4 Upgrade SDL (2.30.9) for Windows 2024-11-20 07:57:35 +01:00
2ec30bdf80 Upgrade FFmpeg (7.1) for Windows
PR #5332 <https://github.com/Genymobile/scrcpy/pull/5332>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-20 07:55:13 +01:00
145b823b1d Add --no-vd-system-decorations
Add an option to disable the following flag for virtual displays:

    DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS

Some devices render a broken UI when this flag is enabled.

Fixes #5494 <https://github.com/Genymobile/scrcpy/issues/5494>
2024-11-20 07:50:45 +01:00
28d64ef319 Fix --new-display bash completion
The option --new-display accepts an optional argument, but bash must not
try to auto-complete it with unrelated content.
2024-11-20 07:49:58 +01:00
36d61f9ecd Reference virtual display documentation
Reference the documentation about virtual displays from the "Display"
section of video.md.
2024-11-19 21:31:04 +01:00
f95a5f97b1 Document filter order
Matrix multiplication is not commutative, so the order of filters
matters.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
adb674a5c8 Add --angle
Add an option to rotate the video content by a custom angle.

Fixes #4135 <https://github.com/Genymobile/scrcpy/issues/4135>
Fixes #4345 <https://github.com/Genymobile/scrcpy/issues/4345>
Refs #4658 <https://github.com/Genymobile/scrcpy/pull/4658>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
d19045628e Remove deprecated options
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
443f315f60 Use natural device orientation for --new-display
If no size is provided with --new-display, the main display size is
used. But the actual size depended on the current device orientation.

To make it deterministic, use the size of the natural device orientation
(portrait for phones, landscape for tablets).

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
0904880816 Log event size mismatch as verbose
On rotation, it is expected that many successive events are ignored due
to size mismatch, when an event was generated from the mirroring window
having the old size, but was received on the device with the new size
(especially since mouse hover events are forwarded).

Do not flood the console with warnings.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
4348f12194 Improve mismatching event size warning
Include both the event size and the current size in the warning message.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
371ff31225 Apply filters to virtual display capture
Apply crop and orientation to virtual display capture.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
456fa510f2 Apply filters to camera capture
Apply crop and orientation to camera capture.

Fixes #4426 <https://github.com/Genymobile/scrcpy/issues/4426>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
45382e3f01 Add --capture-orientation
Deprecate --lock-video-orientation in favor of a more general option
--capture-orientation, which supports all possible orientations
(0, 90, 180, 270, flip0, flip90, flip180, flip270), and a "locked" flag
via a '@' prefix.

All the old "locked video orientations" are supported:
 - --lock-video-orientation      ->  --capture-orientation=@
 - --lock-video-orientation=0    ->  --capture-orientation=@0
 - --lock-video-orientation=90   ->  --capture-orientation=@90
 - --lock-video-orientation=180  ->  --capture-orientation=@180
 - --lock-video-orientation=270  ->  --capture-orientation=@270

In addition, --capture-orientation can rotate/flip the display without
locking, so that it follows the physical device rotation.

For example:

    scrcpy --capture-orientation=flip90

always flips and rotates the capture by 90° clockwise.

The arguments are consistent with --display-orientation and
--record-orientation and --orientation (which provide separate
client-side orientation settings).

Refs #4011 <https://github.com/Genymobile/scrcpy/issues/4011>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
9b03bfc3ae Handle virtual display rotation
Listen to display size changes and rotate the virtual display
accordingly.

Note: use `git show -b` to Show this commit ignoring whitespace changes.

Fixes #5428 <https://github.com/Genymobile/scrcpy/issues/5428>
Refs #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
39d51ff2cc Use DisplayWindowListener for Android 14
On Android 14, DisplayListener may be broken (it never sends events).
This is fixed in recent Android 14 upgrades, but we can't really detect
it directly.

As a workaround, a RotationWatcher and DisplayFoldListener were
registered as a fallback, until a first "display changed" event was
triggered.

To simplify, on Android 14, register a DisplayWindowListener (introduced
in Android 11) to listen to configuration changes instead.

Refs #5455 comment <https://github.com/Genymobile/scrcpy/pull/5455#issuecomment-2481302084>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-19 21:31:04 +01:00
d72686c867 Extract display size monitor
Detecting display size changes is not straightforward:
 - from a DisplayListener, "display changed" events are received, but
   this does not imply that the size has changed (it must be checked);
 - on Android 14 (see e26bdb07a2),
   "display changed" events are not received on some versions, so as a
   fallback, a RotationWatcher and a DisplayFoldListener are registered,
   but unregistered as soon as a "display changed" event is actually
   received, which means that the problem is fixed.

Extract a "display size monitor" to share the code between screen
capture and virtual display capture.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
06385ce83b Reimplement lock orientation using transforms
Reimplement the --lock-video-orientation feature using affine
transforms.

Fixes #4011 <https://github.com/Genymobile/scrcpy/issues/4011>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
9fb0a3dac1 Reimplement crop using transforms
Reimplement the --crop feature using affine transforms.

Fixes #4162 <https://github.com/Genymobile/scrcpy/issues/4162>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
23960ca11a Ignore signalEndOfStream() error
This may be called at any time to interrupt the current encoding,
including when MediaCodec is in an expected state.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
904f86152e Move mediaCodec.stop() to finally block
This will allow stopping MediaCodec only after the cleanup of other
components which must be performed beforehand.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
e226950cfa Make PositionMapper use affine transforms
This will allow applying transformations performed by video filters.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
019ce5eea4 Temporarily ignore lock video orientation and crop
Get rid of old code implementing --lock-video-orientation and --crop
features on the device side.

They will be reimplemented differently.

Refs #4011 <https://github.com/Genymobile/scrcpy/issues/4011>
Refs #4162 <https://github.com/Genymobile/scrcpy/issues/4162>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
d6033d28f5 Split computeVideoSize() into limit() and round8()
Expose two methods on Size directly:
 - limit() to downscale a size;
 - round8() to round both dimensions to multiples of 8.

This will allow removing ScreenInfo completely.

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
89518f49ad Revert "Disable broken options on Android 14"
This reverts commit d62fa8880e.

These options will be reimplemented differently.

Refs #4011 <https://github.com/Genymobile/scrcpy/issues/4011>
Refs #4162 <https://github.com/Genymobile/scrcpy/issues/4162>
PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
2a04858a22 Add on-device OpenGL video filter architecture
Introduce several key components to perform OpenGL filters:
 - OpenGLRunner: a tool for running a filter to be rendered to a Surface
   from an OpenGL-dedicated thread
 - OpenGLFilter: a simple OpenGL filter API
 - AffineOpenGLFilter: a generic OpenGL implementation to apply any 2D
   affine transform
 - AffineMatrix: an affine transform matrix, with helpers to build
   matrices from semantic transformations (rotate, scale, translate…)

PR #5455 <https://github.com/Genymobile/scrcpy/pull/5455>
2024-11-19 21:31:04 +01:00
e411b74a16 Use explicit file protocol for AVIO
AVIO expects a `url` to locate a resource.

Use the file protocol to handle filenames containing colons.

Fixes #5487 <https://github.com/Genymobile/scrcpy/issues/5487>
PR #5499 <https://github.com/Genymobile/scrcpy/pull/5499>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-18 18:48:26 +01:00
5694562a74 Remove duplicate log
The function prepareRetry() already logs a more detailed message:

    Retrying with -mXXXX...
2024-11-18 18:47:57 +01:00
bd9d93194b Pass Options instance directly
Many constructors take a lot of parameters copied from Options. For
simplicity, just pass the Options instance.
2024-11-15 20:16:04 +01:00
794595e3f0 Set displayId to NONE in Options on new display
If a new display is set, force options.getDisplayId() to return
Device.DISPLAY_ID_NONE, to avoid any confusion between a local displayId
and options.getDisplayId().
2024-11-15 20:16:04 +01:00
5e10c37f02 Define all DisplayManager flags locally
For consistency.
2024-11-15 20:16:04 +01:00
0e399b65bd Remove [] around app package names
This simplifies copy-pasting from the result of:

    scrcpy --list-apps
2024-11-15 20:16:04 +01:00
2337f524d1 Improve error message on unknown camera id
If the camera id is explicitly provided (via --camera-id), report a
user-friendly error if no camera with this id is found.
2024-11-15 20:16:04 +01:00
df74cceb6f Use camera prepare() step
For consistency with screen capture.

Refs b60e174780
2024-11-15 20:16:04 +01:00
91373d906b Add FakeContext.getContentResolver()
This avoids the following error on some devices:

    Given calling package android does not match caller's uid 2000

Refs #4639 comment <https://github.com/Genymobile/scrcpy/issues/4639#issuecomment-2466081589>
Fixes #4639 <https://github.com/Genymobile/scrcpy/issues/4639>
PR #5476 <https://github.com/Genymobile/scrcpy/pull/5476>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-14 10:29:03 +01:00
04dd72b594 Add "how to run" link for Windows
Reference the documentation explaining how to run scrcpy on Windows
directly in the main README.
2024-11-13 12:56:35 +01:00
762816cac6 Remove quotes for --video-encoder in documentation
Refs ec602a0334
2024-11-13 12:54:25 +01:00
c0e2e27cf9 Force javac to use UTF-8
The source files are encoded in UTF-8.

Refs <https://github.com/Genymobile/scrcpy/issues/4639#issuecomment-2467206100>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-11-11 09:00:40 +01:00
eff5b4b219 Add --screen-off-timeout
Change the Android "screen off timeout" (the idle delay before the
screen automatically turns off) and restore the initial value on
exit.

PR #5447 <https://github.com/Genymobile/scrcpy/pull/5447>
2024-11-09 10:26:59 +01:00
d3db9c4065 Refactor clean up configuration to simplify
All options were configured dynamically by sending a single byte to an
output stream. But in practice, only the power mode must be changed
dynamically, the others are configured once on start.

For simplicity, pass the value of static options as command line
arguments, and handle dynamic options in a loop only from a separate
thread once the clean up process is started.

This will allow to easily add cleanup options with values which do not
fit in 1 byte.

Also handle the clean up thread (and the loading of initial settings
values) from the CleanUp class, to expose a simpler clean up API.

Refs 9efa162949
PR #5447 <https://github.com/Genymobile/scrcpy/pull/5447>
2024-11-09 10:26:43 +01:00
5936167ff7 Store compensation state as a boolean
We don't need to store the last compensation value anymore, we just need
to know if it's non-zero.
2024-11-04 23:17:43 +01:00
e9dd0f68ad Fix audio regulator compensation
A call to swr_set_compensation() configures the resampler to drop or
duplicate "diff" samples over an interval of "distance" samples.

If the function is not called again, then after "distance" samples, no
more compensation will be applied. So it must always be called, even if
the new computed diff value happens to be the same as the previous one.

In practice, it is unlikely that the diff value is exactly the same
every second, except when it is actively clamped (to 2% of the sample
rate).
2024-11-04 23:09:54 +01:00
104195fc3b Add shortcut to reset video capture/encoding
Reset video capture/encoding on MOD+Shift+r.

Like on device rotation, this starts a new encoding session which
produces a video stream starting by a key frame.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:31:02 +01:00
9958302e6f Interrupt MediaCodec blocking call on reset
When the MediaCodec input is a Surface, no EOS (end-of-stream) will
never occur automatically: it may only be triggered manually by
MediaCodec.signalEndOfInputStream().

Use this signal to interrupt the blocking call to dequeueOutputBuffer()
immediately on reset, without waiting for the next frame to be dequeued.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:27:11 +01:00
69b836930a Handle capture reset via listener
When the capture source becomes "invalid" (because the display size
changes for example), a reset request is performed to restart the
encoder.

The reset state was stored in SurfaceCapture. The capture implementation
set the flag, and the encoder consumed it.

However, this mechanism did not allow a reset request to _interrupt_ the
encoder, which may be waiting on a blocking call (until a new frame is
produced).

To be able to interrupt the encoder, a reset request must not only set a
flag, but run a callback provided by the encoder. For that purpose,
introduce the CaptureListener interface, which is notified by the
SurfaceCapture implementation whenever the capture is invalidated.

For now, the listener implementation just set a flag as before, so the
behavior is unchanged. It lays the groundwork for the next commits.

PR #5432 <https://github.com/Genymobile/scrcpy/pull/5432>
2024-11-03 19:26:55 +01:00
790ea5e58c Check screen on for current displayId
Since Android 14, the "screen on" state can be checked per-display.

Refs <956f4084df%5E!/#F17>
PR #5442 <https://github.com/Genymobile/scrcpy/pull/5442>
2024-11-03 19:10:44 +01:00
1270997f6b Remove useless assignment
The local variable virtualDisplayId was already initialized to the exact
same value.
2024-11-03 19:02:57 +01:00
c905fbba8d Fix indentation 2024-11-03 19:02:37 +01:00
f08a6d86c5 Power on the device only for main display
Power on the device on start only if scrcpy is mirroring the main
display.
2024-11-02 18:51:05 +01:00
3ac4b64461 Register rotation watcher for non-main displays
While moving code, commit 874eaec487 added
a condition `if (displayId == 0)` to register a rotation watcher,
without good reasons.

This condition was kept when the rotation watcher was moved to a
fallback in e26bdb07a2.

Note: use `git show -b` to show this commit ignoring whitespace changes.

Refs #5428 <https://github.com/Genymobile/scrcpy/issues/5428>
2024-11-02 18:49:08 +01:00
c7378f4dc8 Extract setting display power to a separate method
For consistency with the other actions.
2024-10-31 22:49:03 +01:00
e26bdb07a2 Listen to display changed events
Replace RotationWatcher and DisplayFoldListener by a single
DisplayListener, which is notified whenever the display size or dpi
changes.

However, the DisplayListener mechanism is broken in the first versions
of Android 14 (it is fixed in android-14.0.0_r29 by commit [1]), so
continue to use the old mechanism specifically for Android 14 (where
DisplayListener may be broken), until we receive the first
"display changed" event (which proves that it works).

[1]: <5653c6b587%5E%21/>

Fixes #161 <https://github.com/Genymobile/scrcpy/issues/161>
Fixes #1918 <https://github.com/Genymobile/scrcpy/issues/1918>
Fixes #4152 <https://github.com/Genymobile/scrcpy/issues/4152>
Fixes #5362 comment <https://github.com/Genymobile/scrcpy/issues/5362#issuecomment-2416219316>
Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>
PR #5415 <https://github.com/Genymobile/scrcpy/pull/5415>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-10-31 20:12:23 +01:00
04a3e6fb06 Consume reset request on encoding start
If a reset request is pending when a new encoding starts, then it is
implicitly fulfilled.

PR #5415 <https://github.com/Genymobile/scrcpy/pull/5415>
2024-10-31 20:12:23 +01:00
c29ecd0314 Rename --display-buffer to --video-buffer
For consistency with --audio-buffer, rename --display-buffer to
--video-buffer.

Fixes #5403 <https://github.com/Genymobile/scrcpy/issues/5403>
PR #5420 <https://github.com/Genymobile/scrcpy/pull/5420>
2024-10-31 19:57:52 +01:00
d62fa8880e Disable broken options on Android 14
The options --lock-video-orientation and --crop are broken since Android
14. Hopefully, they will be reimplemented differently.

Meanwhile, when running Android >= 14, fail with an error to prevent
incorrect behavior.

Refs #4011 <https://github.com/Genymobile/scrcpy/issues/4011>
Refs #4162 <https://github.com/Genymobile/scrcpy/issues/4162>
PR #5417 <https://github.com/Genymobile/scrcpy/pull/5417>
2024-10-31 19:55:47 +01:00
1f6634ea87 Document adb shell settings commands
Some scrcpy features change Android settings with `adb shell settings`.
Document the commands to execute manually.
2024-10-30 22:23:53 +01:00
58ba00fa06 Adapt "turn screen off" for Android 15
Android 15 introduced an easy way to set the display power:
<fd8b5efc7f%5E!/#F17>

Refs #3927 <https://github.com/Genymobile/scrcpy/issues/3927>
Refs <https://issuetracker.google.com/issues/303565669>
PR #5418 <https://github.com/Genymobile/scrcpy/pull/5418>
2024-10-30 19:38:35 +01:00
569c37cec1 Disable display power for virtual displays
If displayId == Device.DISPLAY_ID_NONE, then the display is virtual: its
power mode cannot be changed.

PR #5418 <https://github.com/Genymobile/scrcpy/pull/5418>
2024-10-30 19:38:07 +01:00
58a0fbbf2e Refactor display power mode
Accept a single boolean "on" rather than a "mode" (which, in practice,
could only take 2 values: NORMAL and OFF).

Also rename "screen power mode" to "display power".

PR #5418 <https://github.com/Genymobile/scrcpy/pull/5418>
2024-10-30 19:38:04 +01:00
acff5b005c Add more details to --list-encoders output
Add more information about each codec (hw/sw, vendor, alias).

Before:

    [server] INFO: List of video encoders:
        --video-codec=h264 --video-encoder=c2.exynos.h264.encoder
        --video-codec=h264 --video-encoder=c2.android.avc.encoder
        --video-codec=h264 --video-encoder=OMX.google.h264.encoder
        --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder
        --video-codec=h265 --video-encoder=c2.android.hevc.encoder
        --video-codec=av1 --video-encoder=c2.google.av1.encoder
        --video-codec=av1 --video-encoder=c2.android.av1.encoder
    // audio encoders omitted

After:

    [server] INFO: List of video encoders:
        --video-codec=h264 --video-encoder=c2.exynos.h264.encoder         (hw) [vendor]
        --video-codec=h264 --video-encoder=c2.android.avc.encoder         (sw)
        --video-codec=h264 --video-encoder=OMX.google.h264.encoder        (sw) (alias for c2.android.avc.encoder)
        --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder         (hw) [vendor]
        --video-codec=h265 --video-encoder=c2.android.hevc.encoder        (sw)
        --video-codec=av1 --video-encoder=c2.google.av1.encoder           (hw) [vendor]
        --video-codec=av1 --video-encoder=c2.android.av1.encoder          (sw)
    // audio encoders omitted

PR #5416 <https://github.com/Genymobile/scrcpy/pull/5416>
2024-10-30 18:51:00 +01:00
5474ae6bd6 Factorize codec info listing
Make the listing of video and audio encoders share the same code.

PR #5416 <https://github.com/Genymobile/scrcpy/pull/5416>
2024-10-30 18:50:47 +01:00
2c25fd7a80 Disable mouse by default if no video playback
If video playback is disabled, then SDK mouse (which uses absolute
positions) could not be used, so the default mouse mode was
automatically switched to UHID.

But UHID does not work on all devices, so it could make the whole scrcpy
session fail.

Instead, disable the mouse by default. It is still possible to pass -M
or --mouse=uhid to enable it explicitly.

Fixes #5410 <https://github.com/Genymobile/scrcpy/issues/5410>
2024-10-29 18:59:29 +01:00
ce21f515e3 Remove unnecessary '\n' in log 2024-10-29 18:58:54 +01:00
381fe95867 Document virtual display and "start app" features
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
566b5be0f6 Add option to start an app by its name
By adding the '?' prefix, the app is searched by its name instead of its
package name (retrieving app names on the device may take some time):

    scrcpy --start-app=?firefox

An app matches if its label starts with the given name,
case-insensitive.

If '+' is also passed to force-stop the app before starting, then the
prefixes must be in that order:

    scrcpy --start-app=+?firefox

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
dd20efa41c Add option to force-stop app before starting
The previous commit introduced:

    scrcpy --start-app=name

By adding a '+' prefix, the app is stopped beforehand:

    scrcpy --start-app=+name

This may be useful to start a fresh app on a new virtual display:

    scrcpy --new-display --start-app=+org.mozilla.firefox

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
13ce277e1f Add --start-app
Add a command line option --start-app=name to start an Android app by
its package name. For example:

    scrcpy --start-app=org.mozilla.firefox

The app will be started on the correct target display:

    scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc

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

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-10-28 18:18:36 +01:00
9c9d92fb1c Add --list-apps
Add an option to list all apps installed on the device:

    scrcpy --list-apps

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
408a388fc5 Reject --new-display for Android <= 10
Fail explicitly if a new virtual display is requested on an Android
version lower than 10.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
98ed5eb643 Add virtual display feature
Add a feature to create a new (separate) virtual display instead of
mirroring the device screen:

    scrcpy --new-display=1920x1080
    scrcpy --new-display=1920x1080/420  # force 420 dpi
    scrcpy --new-display         # use the main display size and density
    scrcpy --new-display -m1920  # scaled to fit a max size of 1920
    scrcpy --new-display=/240    # use the main display size and 240 dpi

Fixes #1887 <https://github.com/Genymobile/scrcpy/issues/1887>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Co-authored-by: anirudhb <anirudhb@users.noreply.github.com>
2024-10-28 18:18:34 +01:00
5d0e012a4c Add DPI to DisplayInfo
It will be useful to automatically set an appropriate DPI for new
virtual displays.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
d19396718e Inject display-related events to virtual display
Mouse and touch events must be sent to the virtual display id (used for
mirroring), other events (like key events) must be sent to the original
display id.

Fixes #4598 <https://github.com/Genymobile/scrcpy/issues/4598>
Fixes #5137 <https://github.com/Genymobile/scrcpy/issues/5137>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>

Co-authored-by: nightmare <mengyanshou@gmail.com>
2024-10-28 18:13:55 +01:00
7024d38199 Send PositionMapper to Controller directly
When a new capture starts, send a new PositionMapper to the Controller
without using the global Device as an intermediate.

Now all Device methods are static.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
f1368d9a8f Introduce PositionMapper
Extract the function that converts coordinates from video space to
display space into a separate component.

It only requires the specific data it uses and does not need a full
ScreenInfo object (although it can be created from a ScreenInfo
instance).

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
d916429566 Move clipboard management to Controller
Continue to declutter the global Device.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
7cfefae5e1 Move implicit displayId to Controller
Remove from Device the functions using an implicit displayId.

Move them to Controller, which knows best which displayId it must use.

This will allow to properly dispatch events either to the origin display
or to the virtual display created for mirroring.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
b60e174780 Add capture prepare() step
Add a function called before each capture starts (before getSize() is
called).

This allows to compute the ScreenInfo instance once exactly when needed.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
5851b62580 Simplify virtual display video size
Do not use an unnecessary intermediate Rect object.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
12d5ca4d5e Move local variables in ScreenCapture
Do not initialize variables when they are not used.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
68e54d9b0b Refactor to call getSize() only once
Avoid to call capture.getSize() (provided by the SurfaceCapture
implementation) twice.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
5f0480c039 Ignore first displayFoldChanged event
An event is posted on registration to signal the initial state.

This had no impact when the listener was registered from Device (before
it was moved to ScreenCapture), because this first initial event was
already triggered when ScreenCapture started listening.

But now, it causes the first encoding to be reset immediately. To avoid
that, ignore the first event.

Refs <https://android.googlesource.com/platform/frameworks/base.git/+/refs/tags/android-15.0.0_r3/services/core/java/com/android/server/policy/DisplayFoldController.java#138>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
874eaec487 Move screen-related features out of Device.java
Move the code related to screen size and rotation/fold to ScreenCapture.

For now, keep the ScreenInfo instance in the Device class to communicate
with the Controller, but it will be removed by further commits.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
14e5439dee Update mouse documentation about capture key
The mouse capture keys are not hardcoded anymore, they use the
configured shortcut modifiers.

Refs ff9fb5994d
2024-10-28 14:55:55 +01:00
a5844e198e Add GitHub Actions release workflow
Fixes #4490 <https://github.com/Genymobile/scrcpy/issues/4490>
PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:31:24 +01:00
2687d20280 Rework release.mk for CI
Make it possible to build scrcpy-server and Windows binaries in
parallel from different GitHub Actions workflows, and to package
everything as a final step.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:29:13 +01:00
9c0a328498 Build server without meson in release.mk
This avoids to install meson/ninja to build scrcpy-server on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:38 +01:00
02ef3d57ce Split client and server tests in release.mk
This will allow to run server tests separately on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:26 +01:00
538a32a539 Fix .PHONY in release.mk
The prepare-deps recipe does not exist anymore. It has been split into
prepare-deps-win32 and prepare-deps-win64.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:13 +01:00
67d4dfb5ff Add missing client build dependency in Fedora
PR #5147 <https://github.com/Genymobile/scrcpy/pull/5147>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-10-23 20:05:52 +02:00
9578aae34e Use explicit constant for @TargetApi 2024-10-22 19:47:56 +02:00
7b3dd595b4 Remove useless version check
Scrcpy requires Android 5.0+, so there is no point testing for older
versions.

Btw, there were two mistakes:
 - the constant name in AndroidVersions should have been
   API_20_ANDROID_4_4W (Android 4.4 without 'W' is API 19)
 - the method isInteractive() was introduced in Android 5.0, not 4.4W:
   <037c33eae7%5E%21/>
2024-10-21 18:51:35 +02:00
a46150f753 Upgrade Android SDK to 35
Also adapt the call to build-tools/35.0.0/aidl, which now requires an
import path (-I. for the current directory).

Otherwise, it fails with:

    ERROR: android/view/IRotationWatcher.aidl:23.1-10: directory ./ is not found in any of the import paths:
     -

Also upgrade AGP (8.7.1) and Gradle (8.9), required for SDK 35.

<https://developer.android.com/build/releases/gradle-plugin#updating-gradle>
2024-10-20 15:26:19 +02:00
3acffaae57 Use explicit constants for Android versions
Who remembers code names? This avoids to check the mapping every time.
2024-10-20 13:15:22 +02:00
e33be3d288 Fix SDL_events.h include
All SDL includes must be prefixed by "SDL2/".

Fixed #5372 <https://github.com/Genymobile/scrcpy/issues/5372>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-10-12 15:57:24 +02:00
c15df01171 Reject non-positive camera sizes early
Throw an exception on parsing if the camera size dimensions are not both
positive.
2024-10-11 22:51:15 +02:00
09741bc805 Do not duplicate server string params
The server params were passed from the main thread to the server thread,
so a deep copy was performed in case the caller instance was destroyed.

But in practice, it only contains memory that lives until the end of the
program (command line arguments), so simply reference it. Several copies
of string fields were missing anyway.
2024-10-11 22:42:39 +02:00
afbaf59abb Cast to sc_tick type in conversion macros
With the old macros definitions, the type of the result depended on the
type of `sec`.

In particular, if sec is a 32-bit type, sec * 1000000 was likely to
overflow (even if the result was assigned to a sc_tick by the caller of
the macro).

This was the case on Windows, where the long type is a 32-bit signed
integer: the --time-limit argument, expressed in seconds, was first
parsed to a long value, then multiplied by 1000000 by the
SC_TICK_FROM_SEC() macro, causing an overflow when the value was greater
than 2147 (2^31 / 1000000).

Fixes #5355 <https://github.com/Genymobile/scrcpy/issues/5355>
2024-10-08 18:18:05 +02:00
5b10650f22 Fix time-limit early interruption
If a value for --time-limit was set, then the thread was not interrupted
on stop (the condvar was not signaled).
2024-10-08 18:12:55 +02:00
Yan
0d8014be52 Fix build error on macOS
Fix window access typo for macOS.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-10-07 16:54:15 +02:00
064670ab4c Add missing include common.h 2024-10-06 18:07:05 +02:00
ff9fb5994d Use shortcut mods as mouse capture keys
Instead of using separate hardcoded keys for mouse capture/uncapture,
use the shortcut mods.

By changing the shortcut mods (for example --shortcut-mod=rctrl), it
allows to forward Alt and Super to the device.

Fixes #5318 <https://github.com/Genymobile/scrcpy/issues/5318>
PR #5322 <https://github.com/Genymobile/scrcpy/pull/5322>
2024-10-06 18:01:02 +02:00
a36de26969 Move shortcut mod functions to a separate header
This will allow to reuse it for mouse capture keys, which are handled by
a component separate from the input manager.

PR #5322 <https://github.com/Genymobile/scrcpy/pull/5322>
2024-10-06 18:00:59 +02:00
281fcc7052 Extract mouse capture
Factorize mouse capture for relative mouse mode to reduce code
duplication between normal and OTG modes.

PR #5322 <https://github.com/Genymobile/scrcpy/pull/5322>
2024-10-06 18:00:56 +02:00
65fc53eace Simplify (and inline) is_shortcut_mod()
Masking was unnecessary (im->sdl_shortcut_mods is implicitly masked).

PR #5322 <https://github.com/Genymobile/scrcpy/pull/5322>
2024-10-06 18:00:52 +02:00
a6f74d72f5 Forward Alt and Super with SDK keyboard
Alt and Super (also named Meta) modifier keys are captured for shortcuts
by default (cf --shortcut-mod).

However, when shortcut modifiers are changed, Alt and Super should be
forwarded to the device. This is the case for AOA and UHID keyboards,
but it was not the case for SDK keyboard.

Fixes #5318 <https://github.com/Genymobile/scrcpy/issues/5318>
PR #5322 <https://github.com/Genymobile/scrcpy/pull/5322>
2024-10-06 18:00:25 +02:00
e724ff4349 Simplify IO.writeFully() for Android >= 6
Do not handle buffer properties manually for Android >= 6 (where it is
already handled by Os.write()).

Refs <d9f7e57f5d%5E%21/>
2024-10-01 22:50:34 +02:00
79014143b9 Fix IO.writeFully() on Android 5
Os.write() did not update the ByteBuffer position before Android 6.

A workaround was added by commit
b882322f73, which fixed part of the
problem, but the position was still not updated across calls, causing
the wrong chunk to be written.

Refs <d9f7e57f5d%5E%21/>
2024-10-01 22:49:55 +02:00
c0a6432967 Extract EINTR handling for Os.write()
Expose a function which retries automatically on EINTR, and throws an
IOException on other errors.
2024-10-01 22:39:06 +02:00
ec602a0334 Suggest command line arguments without quotes
Replace argument suggestion:

    --video-encoder='c2.android.avc.encoder'

by:

    --video-encoder=c2.android.avc.encoder

On Linux, the quotes are interpreted by the shell, but on Windows they
are passed as is.

This was harmless, because even transmitted as is, they were interpreted
by the shell on the device. However, special characters are now
validated since commit bec3321fff, making
the command fail.

Fixes #5329 <https://github.com/Genymobile/scrcpy/issues/5329>
2024-09-30 08:16:33 +02:00
7a9ea5c66f Add shortcut for horizontal tilt
Use Ctrl+Shift for horizontal tilt.

Refs #4529 comment <https://github.com/Genymobile/scrcpy/pull/4529#issuecomment-1855397118>
Fixes #5317 <https://github.com/Genymobile/scrcpy/issues/5317>
2024-09-25 21:38:09 +02:00
d92b7a6024 Rename switch_fullscreen() to toggle_fullscreen()
Toggle means to switch between two states.
2024-09-25 19:59:49 +02:00
0bb3955b95 Split audio player
The audio player had 2 roles:
 - handle the SDL audio output device;
 - resample input samples to maintain a target latency.

Extract the latter to a separate component (an "audio regulator"),
independent of SDL.
2024-09-23 23:59:08 +02:00
62776fb261 Make audio buffering independant of output buffer
This will allow to extract the "audio regulator" part from the audio
player.
2024-09-23 23:59:08 +02:00
10f60054ac Use exact-width integer types 2024-09-23 23:59:08 +02:00
42fb947780 Use local mutex for audio player
Replace SDL_LockAudioDevice() by a local mutex, to minimize the lock
section and to make the code independent of SDL.
2024-09-23 23:59:08 +02:00
2e7a15a998 Remove unused audio player fields
They are only used locally.
2024-09-23 23:59:08 +02:00
a7e61fb871 Remove unused audio player callbacks
The callbacks were never used: the player can report errors directly
from sc_audio_player_frame_sink_push().
2024-09-23 23:59:08 +02:00
0cc6f6aa09 Detect codec/encoder mismatch
Fail with an explicit error when the requested encoder does not match
the requested codec.

Refs #5066 <https://github.com/Genymobile/scrcpy/issues/5066>
2024-09-20 08:45:46 +02:00
f69ac40534 Reorganize server imports
Moving classes into subpackages changed the expected imports order.

Reorganize them all at once automatically to avoid spurious changes in
future commits.
2024-09-20 08:43:42 +02:00
142 changed files with 6420 additions and 2395 deletions

502
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,502 @@
name: Build
on:
workflow_dispatch:
inputs:
name:
description: 'Version name (default is ref name)'
env:
# $VERSION is used by release scripts
VERSION: ${{ github.event.inputs.name || github.ref_name }}
jobs:
test-scrcpy-server:
runs-on: ubuntu-latest
env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Test scrcpy-server
run: release/test_server.sh
build-scrcpy-server:
runs-on: ubuntu-latest
env:
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build
run: release/build_server.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/scrcpy-server
test-build-scrcpy-server-without-gradle:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build without gradle
run: server/build_without_gradle.sh
test-client:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
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
- name: Test
run: release/test_client.sh
build-linux-x86_64:
runs-on: ubuntu-latest
steps:
- name: Check architecture
run: |
arch=$(uname -m)
if [[ "$arch" != x86_64 ]]
then
echo "Unexpected architecture: $arch" >&2
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
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
- name: Build
run: release/build_linux.sh x86_64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-linux-x86_64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-linux-x86_64-intermediate
path: release/work/build-linux-x86_64/dist-tar/
build-win32:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
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 \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- name: Build
run: release/build_windows.sh 32
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-win32
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-win32-intermediate
path: release/work/build-win32/dist-tar/
build-win64:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
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 \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
- name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- name: Build
run: release/build_windows.sh 64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-win64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-win64-intermediate
path: release/work/build-win64/dist-tar/
build-macos-aarch64:
runs-on: macos-latest
steps:
- name: Check architecture
run: |
arch=$(uname -m)
if [[ "$arch" != arm64 ]]
then
echo "Unexpected architecture: $arch" >&2
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
- name: Build
run: release/build_macos.sh aarch64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-macos-aarch64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-macos-aarch64-intermediate
path: release/work/build-macos-aarch64/dist-tar/
build-macos-x86_64:
runs-on: macos-13
steps:
- name: Check architecture
run: |
arch=$(uname -m)
if [[ "$arch" != x86_64 ]]
then
echo "Unexpected architecture: $arch" >&2
exit 1
fi
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: brew install meson ninja nasm libiconv zlib automake
# autoconf and libtool are already installed on macos-13
- name: Build
run: release/build_macos.sh x86_64
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-macos-x86_64
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: build-macos-x86_64-intermediate
path: release/work/build-macos-x86_64/dist-tar/
package-linux-x86_64:
needs:
- build-scrcpy-server
- build-linux-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-linux-x86_64
uses: actions/download-artifact@v4
with:
name: build-linux-x86_64-intermediate
path: release/work/build-linux-x86_64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-linux-x86_64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh linux-x86_64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-linux-x86_64
path: release/output/
package-win32:
needs:
- build-scrcpy-server
- build-win32
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-win32
uses: actions/download-artifact@v4
with:
name: build-win32-intermediate
path: release/work/build-win32/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-win32
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh win32 zip
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-win32
path: release/output/
package-win64:
needs:
- build-scrcpy-server
- build-win64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-win64
uses: actions/download-artifact@v4
with:
name: build-win64-intermediate
path: release/work/build-win64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-win64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh win64 zip
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-win64
path: release/output
package-macos-aarch64:
needs:
- build-scrcpy-server
- build-macos-aarch64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-macos-aarch64
uses: actions/download-artifact@v4
with:
name: build-macos-aarch64-intermediate
path: release/work/build-macos-aarch64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-macos-aarch64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh macos-aarch64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-macos-aarch64
path: release/output/
package-macos-x86_64:
needs:
- build-scrcpy-server
- build-macos-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download build-macos
uses: actions/download-artifact@v4
with:
name: build-macos-x86_64-intermediate
path: release/work/build-macos-x86_64/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-macos-x86_64
tar xf dist-tar/dist.tar.gz
- name: Package
run: release/package_client.sh macos-x86_64 tar.gz
- name: Upload release
uses: actions/upload-artifact@v4
with:
name: release-macos-x86_64
path: release/output/
release:
needs:
- build-scrcpy-server
- package-linux-x86_64
- package-win32
- package-win64
- package-macos-aarch64
- package-macos-x86_64
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download scrcpy-server
uses: actions/download-artifact@v4
with:
name: scrcpy-server
path: release/work/build-server/server/
- name: Download release-linux-x86_64
uses: actions/download-artifact@v4
with:
name: release-linux-x86_64
path: release/output/
- name: Download release-win32
uses: actions/download-artifact@v4
with:
name: release-win32
path: release/output/
- name: Download release-win64
uses: actions/download-artifact@v4
with:
name: release-win64
path: release/output/
- name: Download release-macos-aarch64
uses: actions/download-artifact@v4
with:
name: release-macos-aarch64
path: release/output/
- name: Download release-macos-x86_64
uses: actions/download-artifact@v4
with:
name: release-macos-x86_64
path: release/output/
- name: Package server
run: release/package_server.sh
- name: Generate checksums
run: release/generate_checksums.sh
- name: Upload release artifact
uses: actions/upload-artifact@v4
with:
name: scrcpy-release-${{ env.VERSION }}
path: release/output

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v2.7)
# scrcpy (v3.0)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -31,6 +31,7 @@ It focuses on:
Its features include:
- [audio forwarding](doc/audio.md) (Android 11+)
- [recording](doc/recording.md)
- [virtual display](doc/virtual_display.md)
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
- [copy-paste](doc/control.md#copy-paste) in both directions
- [configurable quality](doc/video.md)
@ -73,7 +74,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
## Get the app
- [Linux](doc/linux.md)
- [Windows](doc/windows.md)
- [Windows](doc/windows.md) (read [how to run](doc/windows.md#run))
- [macOS](doc/macos.md)
@ -91,6 +92,12 @@ Here are just some common examples.
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version
```
- Start VLC in a new virtual display (separate from the device display):
```bash
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
```
- Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
file:
@ -134,6 +141,7 @@ documented in the following pages:
- [Device](doc/device.md)
- [Window](doc/window.md)
- [Recording](doc/recording.md)
- [Virtual display](doc/virtual_display.md)
- [Tunnels](doc/tunnels.md)
- [OTG](doc/otg.md)
- [Camera](doc/camera.md)
@ -173,6 +181,7 @@ to your problem immediately.
You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
- BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social)
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View File

@ -2,6 +2,7 @@ _scrcpy() {
local cur prev words cword
local opts="
--always-on-top
--angle
--audio-bit-rate=
--audio-buffer=
--audio-codec=
@ -17,10 +18,10 @@ _scrcpy() {
--camera-fps=
--camera-high-speed
--camera-size=
--capture-orientation=
--crop=
-d --select-usb
--disable-screensaver
--display-buffer=
--display-id=
--display-orientation=
-e --select-tcpip
@ -33,12 +34,11 @@ _scrcpy() {
--keyboard=
--kill-adb-on-close
--legacy-paste
--list-apps
--list-camera-sizes
--list-cameras
--list-displays
--list-encoders
--lock-video-orientation
--lock-video-orientation=
-m --max-size=
-M
--max-fps=
@ -46,6 +46,8 @@ _scrcpy() {
--mouse-bind=
-n --no-control
-N --no-playback
--new-display
--new-display=
--no-audio
--no-audio-playback
--no-cleanup
@ -55,6 +57,7 @@ _scrcpy() {
--no-mipmaps
--no-mouse-hover
--no-power-on
--no-vd-system-decorations
--no-video
--no-video-playback
--orientation=
@ -75,7 +78,9 @@ _scrcpy() {
--rotation=
-s --serial=
-S --turn-screen-off
--screen-off-timeout=
--shortcut-mod=
--start-app=
-t --show-touches
--tcpip
--tcpip=
@ -86,6 +91,7 @@ _scrcpy() {
--v4l2-sink=
-v --version
-V --verbosity=
--video-buffer=
--video-codec=
--video-codec-options=
--video-encoder=
@ -133,6 +139,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return
;;
--capture-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur"))
return
;;
--orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return
@ -141,10 +151,6 @@ _scrcpy() {
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return
;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
return
;;
--pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return
@ -187,9 +193,9 @@ _scrcpy() {
|--camera-size \
|--crop \
|--display-id \
|--display-buffer \
|--max-fps \
|-m|--max-size \
|--new-display \
|-p|--port \
|--push-target \
|--rotation \
@ -197,6 +203,7 @@ _scrcpy() {
|--tunnel-port \
|--v4l2-buffer \
|--v4l2-sink \
|--video-buffer \
|--video-codec-options \
|--video-encoder \
|--tcpip \

View File

@ -9,6 +9,7 @@ local arguments
arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--angle=[Rotate the video content by a custom angle, in degrees]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
@ -24,10 +25,10 @@ arguments=(
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]'
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
'--display-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]'
@ -40,11 +41,11 @@ arguments=(
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-apps[List Android apps installed on the device]'
'--list-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]'
@ -52,6 +53,7 @@ arguments=(
'--mouse-bind=[Configure bindings of secondary clicks]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--new-display=[Create a new display]'
'--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]'
@ -61,6 +63,7 @@ arguments=(
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]'
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
@ -79,7 +82,9 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
'--screen-off-timeout=[Set the screen off timeout in seconds]'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
'--start-app=[Start an Android app]'
{-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
'--time-limit=[Set the maximum mirroring time, in seconds]'
@ -89,6 +94,7 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-v,--version}'[Print the version of scrcpy]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]'

29
app/deps/adb_linux.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-linux.zip
PROJECT_DIR=platform-tools-$VERSION-linux
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/adb-linux"
cd "$INSTALL_DIR/adb-linux"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/"

29
app/deps/adb_macos.sh Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-darwin.zip
PROJECT_DIR=platform-tools-$VERSION-darwin
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
mkdir -p "$PROJECT_DIR"
cd "$PROJECT_DIR"
ZIP_PREFIX=platform-tools
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/adb-macos"
cd "$INSTALL_DIR/adb-macos"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/"

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=35.0.0
FILENAME=platform-tools_r$VERSION-windows.zip
PROJECT_DIR=platform-tools-$VERSION
SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e
VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
cd "$SOURCES_DIR"
@ -27,6 +27,6 @@ else
rmdir "$ZIP_PREFIX"
fi
mkdir -p "$INSTALL_DIR/$HOST/bin"
cd "$INSTALL_DIR/$HOST/bin"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/"
mkdir -p "$INSTALL_DIR/adb-windows"
cd "$INSTALL_DIR/adb-windows"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/"

View File

@ -1,15 +1,35 @@
#!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed
if [[ $# != 1 ]]
process_args() {
if [[ $# != 3 ]]
then
# <host>: win32 or win64
echo "Syntax: $0 <host>" >&2
# <build_type>: native or cross
# <link_type>: static or shared
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
exit 1
fi
HOST="$1"
BUILD_TYPE="$2" # native or cross
LINK_TYPE="$3" # static or shared
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
then
echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2
exit 1
fi
if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]]
then
echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2
exit 1
fi
if [[ "$BUILD_TYPE" == cross ]]
then
if [[ "$HOST" = win32 ]]
then
HOST_TRIPLET=i686-w64-mingw32
@ -17,9 +37,11 @@ elif [[ "$HOST" = win64 ]]
then
HOST_TRIPLET=x86_64-w64-mingw32
else
echo "Unsupported host: $HOST" >&2
echo "Unsupported cross-build to host: $HOST" >&2
exit 1
fi
fi
}
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
@ -37,7 +59,7 @@ checksum() {
local file="$1"
local sum="$2"
echo "$file: verifying checksum..."
echo "$sum $file" | sha256sum -c
echo "$sum $file" | shasum -a256 -c
}
get_file() {

View File

@ -3,11 +3,12 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=7.0.2
VERSION=7.1
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
cd "$SOURCES_DIR"
@ -22,68 +23,121 @@ fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR"
if [[ "$HOST" = win32 ]]
if [[ -d "$DIRNAME" ]]
then
ARCH=x86
elif [[ "$HOST" = win64 ]]
then
ARCH=x86_64
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
echo "Unsupported host: $HOST" >&2
exit 1
fi
mkdir "$DIRNAME"
cd "$DIRNAME"
if [[ "$HOST" == win* ]]
then
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
# -static to avoid dynamic dependency to zlib
export CFLAGS='-static-libgcc -static'
export CXXFLAGS="$CFLAGS"
export LDFLAGS='-static-libgcc -static'
if [[ -d "$HOST" ]]
elif [[ "$HOST" == "macos" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include"
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--enable-cross-compile \
--target-os=mingw32 \
--arch="$ARCH" \
--cross-prefix="${HOST_TRIPLET}-" \
--cc="${HOST_TRIPLET}-gcc" \
--extra-cflags="-O2 -fPIC" \
--enable-shared \
--disable-static \
--disable-programs \
--disable-doc \
--disable-swscale \
--disable-postproc \
--disable-avfilter \
--disable-avdevice \
--disable-network \
--disable-everything \
--enable-swresample \
--enable-decoder=h264 \
--enable-decoder=hevc \
--enable-decoder=av1 \
--enable-decoder=pcm_s16le \
--enable-decoder=opus \
--enable-decoder=aac \
--enable-decoder=flac \
--enable-decoder=png \
--enable-protocol=file \
--enable-demuxer=image2 \
--enable-parser=png \
--enable-zlib \
--enable-muxer=matroska \
--enable-muxer=mp4 \
--enable-muxer=opus \
--enable-muxer=flac \
--enable-muxer=wav \
export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include"
export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
fi
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
--extra-cflags="-O2 -fPIC"
--disable-programs
--disable-doc
--disable-swscale
--disable-postproc
--disable-avfilter
--disable-network
--disable-everything
--disable-vulkan
--disable-vaapi
--disable-vdpau
--enable-swresample
--enable-decoder=h264
--enable-decoder=hevc
--enable-decoder=av1
--enable-decoder=pcm_s16le
--enable-decoder=opus
--enable-decoder=aac
--enable-decoder=flac
--enable-decoder=png
--enable-protocol=file
--enable-demuxer=image2
--enable-parser=png
--enable-zlib
--enable-muxer=matroska
--enable-muxer=mp4
--enable-muxer=opus
--enable-muxer=flac
--enable-muxer=wav
)
if [[ "$HOST" == linux ]]
then
conf+=(
--enable-libv4l2
--enable-outdev=v4l2
--enable-encoder=rawvideo
)
else
# libavdevice is only used for V4L2 on Linux
conf+=(
--disable-avdevice
)
fi
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--enable-cross-compile
--cross-prefix="${HOST_TRIPLET}-"
--cc="${HOST_TRIPLET}-gcc"
)
case "$HOST" in
win32)
conf+=(
--target-os=mingw32
--arch=x86
)
;;
win64)
conf+=(
--target-os=mingw32
--arch=x86_64
)
;;
*)
echo "Unsupported host: $HOST" >&2
exit 1
esac
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
fi
make -j

View File

@ -3,6 +3,7 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=1.0.27
FILENAME=libusb-$VERSION.tar.gz
@ -25,20 +26,40 @@ cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS"
if [[ -d "$HOST" ]]
if [[ -d "$DIRNAME" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
mkdir "$HOST"
cd "$HOST"
mkdir "$DIRNAME"
cd "$DIRNAME"
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
)
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--host="$HOST_TRIPLET"
)
fi
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--host="$HOST_TRIPLET" \
--enable-shared \
--disable-static
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
fi
make -j

View File

@ -3,11 +3,12 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
process_args "$@"
VERSION=2.30.7
VERSION=2.30.9
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5
SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407
cd "$SOURCES_DIR"
@ -25,23 +26,54 @@ cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS"
if [[ -d "$HOST" ]]
if [[ -d "$DIRNAME" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
cd "$DIRNAME"
else
mkdir "$HOST"
cd "$HOST"
mkdir "$DIRNAME"
cd "$DIRNAME"
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--host="$HOST_TRIPLET" \
--enable-shared \
conf=(
--prefix="$INSTALL_DIR/$DIRNAME"
)
if [[ "$HOST" == linux ]]
then
conf+=(
--enable-video-wayland
--enable-video-x11
)
fi
if [[ "$LINK_TYPE" == static ]]
then
conf+=(
--enable-static
--disable-shared
)
else
conf+=(
--disable-static
--enable-shared
)
fi
if [[ "$BUILD_TYPE" == cross ]]
then
conf+=(
--host="$HOST_TRIPLET"
)
fi
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
fi
make -j
# There is no "make install-strip"
make install
# Strip manually
${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll"
if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]]
then
${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll"
fi

View File

@ -5,6 +5,7 @@ src = [
'src/adb/adb_parser.c',
'src/adb/adb_tunnel.c',
'src/audio_player.c',
'src/audio_regulator.c',
'src/cli.c',
'src/clock.c',
'src/compat.c',
@ -22,6 +23,7 @@ src = [
'src/frame_buffer.c',
'src/input_manager.c',
'src/keyboard_sdk.c',
'src/mouse_capture.c',
'src/mouse_sdk.c',
'src/opengl.c',
'src/options.c',
@ -44,6 +46,7 @@ src = [
'src/util/acksync.c',
'src/util/audiobuf.c',
'src/util/average.c',
'src/util/env.c',
'src/util/file.c',
'src/util/intmap.c',
'src/util/intr.c',
@ -107,20 +110,22 @@ endif
cc = meson.get_compiler('c')
static = get_option('static')
dependencies = [
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
dependency('libavformat', version: '>= 57.33', static: static),
dependency('libavcodec', version: '>= 57.37', static: static),
dependency('libavutil', static: static),
dependency('libswresample', static: static),
dependency('sdl2', version: '>= 2.0.5', static: static),
]
if v4l2_support
dependencies += dependency('libavdevice')
dependencies += dependency('libavdevice', static: static)
endif
if usb_support
dependencies += dependency('libusb-1.0')
dependencies += dependency('libusb-1.0', static: static)
endif
if host_machine.system() == 'windows'
@ -165,9 +170,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
# enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support)

View File

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

View File

@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP
.B \-\-always\-on\-top
Make scrcpy window always on top (above other windows).
.TP
.BI "\-\-angle " degrees
Rotate the video content by a custom angle, in degrees (clockwise).
.TP
.BI "\-\-audio\-bit\-rate " value
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
@ -93,18 +97,6 @@ Select the camera size by its aspect ratio (+/- 10%).
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
.TP
.B \-\-camera\-high\-speed
Enable high-speed camera capture mode.
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
.TP
.BI "\-\-camera\-id " id
Specify the device camera id to mirror.
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP
.BI "\-\-camera\-facing " facing
Select the device camera by its facing direction.
@ -117,17 +109,39 @@ Specify the camera capture frame rate.
If not specified, Android's default frame rate (30 fps) is used.
.TP
.B \-\-camera\-high\-speed
Enable high-speed camera capture mode.
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
.TP
.BI "\-\-camera\-id " id
Specify the device camera id to mirror.
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
.TP
.BI "\-\-capture\-orientation " value
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation.
If '@' is passed alone, then the rotation is locked to the initial device orientation.
Default is 0.
.TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any
.B \-\-max\-size
value is computed on the cropped size.
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).
.TP
.B \-d, \-\-select\-usb
@ -139,12 +153,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display\-buffer " ms
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
Default is 0 (no buffering).
.TP
.BI "\-\-display\-id " id
Specify the device display id to mirror.
@ -227,6 +235,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-apps
List Android apps installed on the device.
.TP
.B \-\-list\-camera\-sizes
List the valid camera capture sizes.
@ -243,16 +255,6 @@ List video and audio encoders available on the device.
.B \-\-list\-displays
List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
Lock capture video orientation to \fIvalue\fR.
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
@ -314,6 +316,17 @@ Disable device control (mirror the device in read\-only).
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
.TP
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]]
Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI.
Examples:
\-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420
\-\-new\-display # main display size and density
\-\-new\-display=/240 # main display size and 240 dpi
.TP
.B \-\-no\-audio
Disable audio forwarding.
@ -356,6 +369,10 @@ Do not forward mouse hover (mouse motion without any clicks) events.
.B \-\-no\-power\-on
Do not power on the device on start.
.TP
.B \-\-no\-vd\-system\-decorations
Disable virtual display system decorations flag.
.TP
.B \-\-no\-video
Disable video forwarding.
@ -478,6 +495,22 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-start\-app " name
Start an Android app, by its exact package name.
Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time):
scrcpy --start-app=?firefox
Add a '+' prefix to force-stop before starting the app:
scrcpy --new-display --start-app=+org.mozilla.firefox
Both prefixes can be used, in that order:
scrcpy --start-app=+?firefox
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit.
@ -522,13 +555,19 @@ Default is "info" for release builds, "debug" for debug builds.
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
.TP
.BI "\-\-v4l2-buffer " ms
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
.TP
.BI "\-\-video\-buffer " ms
Add a buffering delay (in milliseconds) before displaying video frames.
This increases latency to compensate for jitter.
Default is 0 (no buffering).
@ -637,6 +676,10 @@ Pause or re-pause display
.B MOD+Shift+z
Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP
.B MOD+g
Resize window to 1:1 (pixel\-perfect)
@ -727,7 +770,11 @@ Pinch-to-zoom and rotate from the center of the screen
.TP
.B Shift+click-and-move
Tilt (slide vertically with two fingers)
Tilt vertically (slide with 2 fingers)
.TP
.B Ctrl+Shift+click-and-move
Tilt horizontally (slide with 2 fingers)
.TP
.B Drag & drop APK file

View File

@ -7,6 +7,7 @@
#include "adb_device.h"
#include "adb_parser.h"
#include "util/env.h"
#include "util/file.h"
#include "util/log.h"
#include "util/process_intr.h"
@ -24,15 +25,45 @@
*/
#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL }
static const char *adb_executable;
static char *adb_executable;
bool
sc_adb_init(void) {
adb_executable = sc_get_env("ADB");
if (adb_executable) {
LOGD("Using adb: %s", adb_executable);
return true;
}
#if !defined(PORTABLE) || defined(_WIN32)
adb_executable = strdup("adb");
if (!adb_executable) {
LOG_OOM();
return false;
}
#else
// For portable builds, use the absolute path to the adb executable
// in the same directory as scrcpy (except on Windows, where "adb"
// is sufficient)
adb_executable = sc_file_get_local_path("adb");
if (!adb_executable) {
// Error already logged
return false;
}
LOGD("Using adb (portable): %s", adb_executable);
#endif
return true;
}
void
sc_adb_destroy(void) {
free(adb_executable);
}
const char *
sc_adb_get_executable(void) {
if (!adb_executable) {
adb_executable = getenv("ADB");
if (!adb_executable)
adb_executable = "adb";
}
return adb_executable;
}
@ -739,3 +770,21 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf);
}
uint16_t
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) {
char *sdk_version =
sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT);
if (!sdk_version) {
return 0;
}
long value;
bool ok = sc_str_parse_integer(sdk_version, &value);
free(sdk_version);
if (!ok || value < 0 || value > 0xFFFF) {
return 0;
}
return value;
}

View File

@ -15,6 +15,12 @@
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR)
bool
sc_adb_init(void);
void
sc_adb_destroy(void);
const char *
sc_adb_get_executable(void);
@ -114,4 +120,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
char *
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
/**
* Return the device SDK version.
*/
uint16_t
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial);
#endif

View File

@ -1,138 +1,23 @@
#include "audio_player.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include "util/log.h"
//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug
/**
* Real-time audio player with configurable latency
*
* As input, the player regularly receives AVFrames of decoded audio samples.
* As output, an SDL callback regularly requests audio samples to be played.
* In the middle, an audio buffer stores the samples produced but not consumed
* yet.
*
* The goal of the player is to feed the audio output with a latency as low as
* possible while avoiding buffer underrun (i.e. not being able to provide
* samples when requested).
*
* The player aims to feed the audio output with as little latency as possible
* while avoiding buffer underrun. To achieve this, it attempts to maintain the
* average buffering (the number of samples present in the buffer) around a
* target value. If this target buffering is too low, then buffer underrun will
* occur frequently. If it is too high, then latency will become unacceptable.
* This target value is configured using the scrcpy option --audio-buffer.
*
* The player cannot adjust the sample input rate (it receives samples produced
* in real-time) or the sample output rate (it must provide samples as
* requested by the audio output callback). Therefore, it may only apply
* compensation by resampling (converting _m_ input samples to _n_ output
* samples).
*
* The compensation itself is applied by libswresample (FFmpeg). It is
* configured using swr_set_compensation(). An important work for the player
* is to estimate the compensation value regularly and apply it.
*
* The estimated buffering level is the result of averaging the "natural"
* buffering (samples are produced and consumed by blocks, so it must be
* smoothed), and making instant adjustments resulting of its own actions
* (explicit compensation and silence insertion on underflow), which are not
* smoothed.
*
* Buffer underflow events can occur when packets arrive too late. In that case,
* the player inserts silence. Once the packets finally arrive (late), one
* strategy could be to drop the samples that were replaced by silence, in
* order to keep a minimal latency. However, dropping samples in case of buffer
* underflow is inadvisable, as it would temporarily increase the underflow
* even more and cause very noticeable audio glitches.
*
* Therefore, the player doesn't drop any sample on underflow. The compensation
* mechanism will absorb the delay introduced by the inserted silence.
*/
/** Downcast frame_sink to sc_audio_player */
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
#define SC_SDL_SAMPLE_FMT AUDIO_F32
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_LockAudioDevice()
assert(len_int > 0);
size_t len = len_int;
uint32_t count = TO_SAMPLES(len);
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif
assert(len % ap->audioreg.sample_size == 0);
uint32_t out_samples = len / ap->audioreg.sample_size;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (!played) {
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
// Wait until the buffer is filled up to at least target_buffering
// before playing
if (buffered_samples < ap->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", count);
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
memset(stream, 0, len);
return;
}
}
uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
if (read < count) {
uint32_t silence = count - read;
// Insert silence. In theory, the inserted silent samples replace the
// missing real samples, which will arrive later, so they should be
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
bool received = atomic_load_explicit(&ap->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering
atomic_fetch_add_explicit(&ap->underflow, silence,
memory_order_relaxed);
}
}
atomic_store_explicit(&ap->played, true, memory_order_relaxed);
}
static uint8_t *
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
size_t min_buf_size = TO_BYTES(min_samples);
if (min_buf_size > ap->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ap->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ap->swr_buf = buf;
ap->swr_buf_alloc_size = new_size;
}
return ap->swr_buf;
sc_audio_regulator_pull(&ap->audioreg, stream, out_samples);
}
static bool
@ -140,209 +25,21 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
const AVFrame *frame) {
struct sc_audio_player *ap = DOWNCAST(sink);
SwrContext *swr_ctx = ap->swr_ctx;
int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples = MIN(ret, dst_nb_samples);
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
if (samples > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
swr_buf += TO_BYTES(samples - cap);
samples = cap;
}
uint32_t skipped_samples = 0;
uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
// All samples that could be written without locking have been written,
// now we need to lock to drop/consume old samples
SDL_LockAudioDevice(ap->device);
// Retry with the lock
written += sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
remaining);
if (written < samples) {
remaining = samples - written;
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
SDL_UnlockAudioDevice(ap->device);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
}
}
uint32_t underflow = 0;
uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (played) {
underflow = atomic_exchange_explicit(&ap->underflow, 0,
memory_order_relaxed);
max_buffered_samples = ap->target_buffering
+ 12 * ap->output_buffer
+ ap->target_buffering / 10;
} else {
// SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
}
uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
SDL_LockAudioDevice(ap->device);
can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
SDL_UnlockAudioDevice(ap->device);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifdef SC_AUDIO_PLAYER_DEBUG
} else {
LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif
}
}
}
atomic_store_explicit(&ap->received, true, memory_order_relaxed);
if (!played) {
// Nothing more to do
return true;
}
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
// The compensation must apply instantly, it must not be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
if (ap->avg_buffering.avg < 0) {
// Since dropping samples instantly reduces buffering, the difference
// is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ap->avg_buffering.avg = 0;
}
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, can_read);
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
can_read, sc_average_get(&ap->avg_buffering));
#endif
ap->samples_since_resync += written;
if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second
ap->samples_since_resync = 0;
float avg = sc_average_get(&ap->avg_buffering);
int diff = ap->target_buffering - avg;
// Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms.
int threshold = ap->compensation != 0
? ap->sample_rate / 1000 /* 1ms */
: ap->sample_rate * 4 / 1000; /* 4ms */
if (abs(diff) < threshold) {
// Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg, can_read, diff);
if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ap->compensation = diff;
}
}
}
return true;
return sc_audio_regulator_push(&ap->audioreg, frame);
}
static bool
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
assert(ctx->ch_layout.nb_channels > 0);
unsigned nb_channels = ctx->ch_layout.nb_channels;
assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256);
uint8_t nb_channels = ctx->ch_layout.nb_channels;
#else
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
assert(tmp > 0);
unsigned nb_channels = tmp;
assert(tmp > 0 && tmp < 256);
uint8_t nb_channels = tmp;
#endif
assert(ctx->sample_rate > 0);
@ -350,17 +47,19 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
assert(out_bytes_per_sample > 0);
ap->sample_rate = ctx->sample_rate;
ap->nb_channels = nb_channels;
ap->out_bytes_per_sample = out_bytes_per_sample;
uint32_t target_buffering_samples =
ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ;
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
/ SC_TICK_FREQ;
size_t sample_size = nb_channels * out_bytes_per_sample;
bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx,
target_buffering_samples);
if (!ok) {
return false;
}
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate
/ SC_TICK_FREQ;
assert(aout_samples <= 0xFFFF);
ap->output_buffer = (uint16_t) aout_samples;
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
@ -375,69 +74,10 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (!ap->device) {
LOGE("Could not open audio device: %s", SDL_GetError());
sc_audio_regulator_destroy(&ap->audioreg);
return false;
}
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
goto error_close_audio_device;
}
ap->swr_ctx = swr_ctx;
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
// Use a ring-buffer of the target buffering size plus 1 second between the
// producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel
// without locking.
uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
if (!ok) {
goto error_free_swr_ctx;
}
size_t initial_swr_buf_size = TO_BYTES(4096);
ap->swr_buf = malloc(initial_swr_buf_size);
if (!ap->swr_buf) {
LOG_OOM();
goto error_destroy_audiobuf;
}
ap->swr_buf_alloc_size = initial_swr_buf_size;
// Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value.
sc_average_init(&ap->avg_buffering, 128);
ap->samples_since_resync = 0;
ap->received = false;
atomic_init(&ap->played, false);
atomic_init(&ap->received, false);
atomic_init(&ap->underflow, 0);
ap->compensation = 0;
// 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);
@ -449,15 +89,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
SDL_PauseAudioDevice(ap->device, 0);
return true;
error_destroy_audiobuf:
sc_audiobuf_destroy(&ap->buf);
error_free_swr_ctx:
swr_free(&ap->swr_ctx);
error_close_audio_device:
SDL_CloseAudioDevice(ap->device);
return false;
}
static void
@ -468,9 +99,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
SDL_PauseAudioDevice(ap->device, 1);
SDL_CloseAudioDevice(ap->device);
free(ap->swr_buf);
sc_audiobuf_destroy(&ap->buf);
swr_free(&ap->swr_ctx);
sc_audio_regulator_destroy(&ap->audioreg);
}
void

View File

@ -5,76 +5,27 @@
#include <stdatomic.h>
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
#include "audio_regulator.h"
#include "trait/frame_sink.h"
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#include "util/tick.h"
struct sc_audio_player {
struct sc_frame_sink frame_sink;
SDL_AudioDeviceID device;
// The target buffering between the producer and the consumer. This value
// is directly use for compensation.
// Since audio capture and/or encoding on the device typically produce
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
// value should be higher.
sc_tick target_buffering_delay;
uint32_t target_buffering; // in samples
// SDL audio output buffer size.
// SDL audio output buffer size
sc_tick output_buffer_duration;
uint16_t output_buffer;
// Audio buffer to communicate between the receiver and the SDL audio
// callback
struct sc_audiobuf buf;
// Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
unsigned sample_rate;
// The number of channels is the same for input and output
unsigned nb_channels;
// The number of bytes per sample for a single channel
size_t out_bytes_per_sample;
// Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow) (only used by
// the receiver thread)
struct sc_average avg_buffering;
// Count the number of samples to trigger a compensation update regularly
// (only used by the receiver thread)
uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Current applied compensation value (only used by the receiver thread)
int compensation;
// Set to true the first time a sample is received
atomic_bool received;
// Set to true the first time the SDL callback is called
atomic_bool played;
const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata;
};
struct sc_audio_player_callbacks {
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
SDL_AudioDeviceID device;
struct sc_audio_regulator audioreg;
};
void

413
app/src/audio_regulator.c Normal file
View File

@ -0,0 +1,413 @@
#include "audio_regulator.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include "util/log.h"
//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug
/**
* Real-time audio regulator with configurable latency
*
* As input, the regulator regularly receives AVFrames of decoded audio samples.
* As output, the audio player regularly requests audio samples to be played.
* In the middle, an audio buffer stores the samples produced but not consumed
* yet.
*
* The goal of the regulator is to feed the audio player with a latency as low
* as possible while avoiding buffer underrun (i.e. not being able to provide
* samples when requested).
*
* To achieve this, it attempts to maintain the average buffering (the number
* of samples present in the buffer) around a target value. If this target
* buffering is too low, then buffer underrun will occur frequently. If it is
* too high, then latency will become unacceptable. This target value is
* configured using the scrcpy option --audio-buffer.
*
* The regulator cannot adjust the sample input rate (it receives samples
* produced in real-time) or the sample output rate (it must provide samples as
* requested by the audio player). Therefore, it may only apply compensation by
* resampling (converting _m_ input samples to _n_ output samples).
*
* The compensation itself is applied by libswresample (FFmpeg). It is
* configured using swr_set_compensation(). An important work for the regulator
* is to estimate the compensation value regularly and apply it.
*
* The estimated buffering level is the result of averaging the "natural"
* buffering (samples are produced and consumed by blocks, so it must be
* smoothed), and making instant adjustments resulting of its own actions
* (explicit compensation and silence insertion on underflow), which are not
* smoothed.
*
* Buffer underflow events can occur when packets arrive too late. In that case,
* the regulator inserts silence. Once the packets finally arrive (late), one
* strategy could be to drop the samples that were replaced by silence, in
* order to keep a minimal latency. However, dropping samples in case of buffer
* underflow is inadvisable, as it would temporarily increase the underflow
* even more and cause very noticeable audio glitches.
*
* Therefore, the regulator doesn't drop any sample on underflow. The
* compensation mechanism will absorb the delay introduced by the inserted
* silence.
*/
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES))
void
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
uint32_t out_samples) {
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples);
#endif
// A lock is necessary in the rare case where the producer needs to drop
// samples already pushed (when the buffer is full)
sc_mutex_lock(&ar->mutex);
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
if (!played) {
uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf);
// Wait until the buffer is filled up to at least target_buffering
// before playing
if (buffered_samples < ar->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", out_samples);
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
memset(out, 0, out_samples * ar->sample_size);
sc_mutex_unlock(&ar->mutex);
return;
}
}
uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples);
sc_mutex_unlock(&ar->mutex);
if (read < out_samples) {
uint32_t silence = out_samples - read;
// Insert silence. In theory, the inserted silent samples replace the
// missing real samples, which will arrive later, so they should be
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
bool received = atomic_load_explicit(&ar->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering
atomic_fetch_add_explicit(&ar->underflow, silence,
memory_order_relaxed);
}
}
atomic_store_explicit(&ar->played, true, memory_order_relaxed);
}
static uint8_t *
sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar,
uint32_t min_samples) {
size_t min_buf_size = TO_BYTES(min_samples);
if (min_buf_size > ar->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ar->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ar->swr_buf = buf;
ar->swr_buf_alloc_size = new_size;
}
return ar->swr_buf;
}
bool
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
SwrContext *swr_ctx = ar->swr_ctx;
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples = MIN(ret, dst_nb_samples);
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif
uint32_t cap = sc_audiobuf_capacity(&ar->buf);
if (samples > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
swr_buf += TO_BYTES(samples - cap);
samples = cap;
}
uint32_t skipped_samples = 0;
uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
// All samples that could be written without locking have been written,
// now we need to lock to drop/consume old samples
sc_mutex_lock(&ar->mutex);
// Retry with the lock
written += sc_audiobuf_write(&ar->buf,
swr_buf + TO_BYTES(written),
remaining);
if (written < samples) {
remaining = samples - written;
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
sc_mutex_unlock(&ar->mutex);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ar->buf,
swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
}
}
uint32_t underflow = 0;
uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
if (played) {
underflow = atomic_exchange_explicit(&ar->underflow, 0,
memory_order_relaxed);
max_buffered_samples = ar->target_buffering * 11 / 10
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
} else {
// Playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
max_buffered_samples = ar->target_buffering
+ 10 * ar->sample_rate / 1000 /* 10 ms */;
}
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
sc_mutex_lock(&ar->mutex);
can_read = sc_audiobuf_can_read(&ar->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
sc_mutex_unlock(&ar->mutex);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifdef SC_AUDIO_REGULATOR_DEBUG
} else {
LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif
}
}
}
atomic_store_explicit(&ar->received, true, memory_order_relaxed);
if (!played) {
// Nothing more to do
return true;
}
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
// The compensation must apply instantly, it must not be smoothed
ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
if (ar->avg_buffering.avg < 0) {
// Since dropping samples instantly reduces buffering, the difference
// is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ar->avg_buffering.avg = 0;
}
// However, the buffering level must be smoothed
sc_average_push(&ar->avg_buffering, can_read);
#ifdef SC_AUDIO_REGULATOR_DEBUG
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
can_read, sc_average_get(&ar->avg_buffering));
#endif
ar->samples_since_resync += written;
if (ar->samples_since_resync >= ar->sample_rate) {
// Recompute compensation every second
ar->samples_since_resync = 0;
float avg = sc_average_get(&ar->avg_buffering);
int diff = ar->target_buffering - avg;
// Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms.
int threshold = ar->compensation_active
? ar->sample_rate / 1000 /* 1ms */
: ar->sample_rate * 4 / 1000; /* 4ms */
if (abs(diff) < threshold) {
// Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ar->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ar->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff);
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ar->compensation_active = diff != 0;
}
}
return true;
}
bool
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
const AVCodecContext *ctx, uint32_t target_buffering) {
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
return false;
}
ar->swr_ctx = swr_ctx;
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
bool ok = sc_mutex_init(&ar->mutex);
if (!ok) {
goto error_free_swr_ctx;
}
ar->target_buffering = target_buffering;
ar->sample_size = sample_size;
ar->sample_rate = ctx->sample_rate;
// Use a ring-buffer of the target buffering size plus 1 second between the
// producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel
// without locking.
uint32_t audiobuf_samples = target_buffering + ar->sample_rate;
ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples);
if (!ok) {
goto error_destroy_mutex;
}
size_t initial_swr_buf_size = TO_BYTES(4096);
ar->swr_buf = malloc(initial_swr_buf_size);
if (!ar->swr_buf) {
LOG_OOM();
goto error_destroy_audiobuf;
}
ar->swr_buf_alloc_size = initial_swr_buf_size;
// Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value.
sc_average_init(&ar->avg_buffering, 128);
ar->samples_since_resync = 0;
ar->received = false;
atomic_init(&ar->played, false);
atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0);
ar->compensation_active = false;
return true;
error_destroy_audiobuf:
sc_audiobuf_destroy(&ar->buf);
error_destroy_mutex:
sc_mutex_destroy(&ar->mutex);
error_free_swr_ctx:
swr_free(&ar->swr_ctx);
return false;
}
void
sc_audio_regulator_destroy(struct sc_audio_regulator *ar) {
free(ar->swr_buf);
sc_audiobuf_destroy(&ar->buf);
sc_mutex_destroy(&ar->mutex);
swr_free(&ar->swr_ctx);
}

71
app/src/audio_regulator.h Normal file
View File

@ -0,0 +1,71 @@
#ifndef SC_AUDIO_REGULATOR_H
#define SC_AUDIO_REGULATOR_H
#include "common.h"
#include <stdatomic.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
struct sc_audio_regulator {
sc_mutex mutex;
// Target buffering between the producer and the consumer (in samples)
uint32_t target_buffering;
// Audio buffer to communicate between the receiver and the player
struct sc_audiobuf buf;
// Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
uint32_t sample_rate;
// The number of bytes per sample (for all channels)
size_t sample_size;
// Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow) (only used by
// the receiver thread)
struct sc_average avg_buffering;
// Count the number of samples to trigger a compensation update regularly
// (only used by the receiver thread)
uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow;
// Non-zero compensation applied (only used by the receiver thread)
bool compensation_active;
// Set to true the first time a sample is received
atomic_bool received;
// Set to true the first time samples are pulled by the player
atomic_bool played;
};
bool
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
const AVCodecContext *ctx, uint32_t target_buffering);
void
sc_audio_regulator_destroy(struct sc_audio_regulator *ar);
bool
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame);
void
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
uint32_t samples);
#endif

View File

@ -50,6 +50,7 @@ enum {
OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER,
OPT_VIDEO_BUFFER,
OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT,
@ -102,6 +103,13 @@ enum {
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
OPT_GAMEPAD,
OPT_NEW_DISPLAY,
OPT_LIST_APPS,
OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT,
OPT_CAPTURE_ORIENTATION,
OPT_ANGLE,
OPT_NO_VD_SYSTEM_DECORATIONS,
};
struct sc_option {
@ -143,6 +151,13 @@ static const struct sc_option options[] = {
.longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).",
},
{
.longopt_id = OPT_ANGLE,
.longopt = "angle",
.argdesc = "degrees",
.text = "Rotate the video content by a custom angle, in degrees "
"(clockwise).",
},
{
.longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate",
@ -240,14 +255,6 @@ static const struct sc_option options[] = {
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")."
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{
.longopt_id = OPT_CAMERA_FACING,
.longopt = "camera-facing",
@ -255,6 +262,14 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".",
},
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{
.longopt_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed",
@ -262,6 +277,14 @@ static const struct sc_option options[] = {
"This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.",
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{
.longopt_id = OPT_CAMERA_SIZE,
.longopt = "camera-size",
@ -269,12 +292,21 @@ static const struct sc_option options[] = {
.text = "Specify an explicit camera capture size.",
},
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.longopt_id = OPT_CAPTURE_ORIENTATION,
.longopt = "capture-orientation",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
.text = "Set the capture video orientation.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
"and flip270, possibly prefixed by '@'.\n"
"The number represents the clockwise rotation in degrees; the "
"flip\" keyword applies a horizontal flip before the "
"rotation.\n"
"If a leading '@' is passed (@90) for display capture, then "
"the rotation is locked, and is relative to the natural device "
"orientation.\n"
"If '@' is passed alone, then the rotation is locked to the "
"initial device orientation.\n"
"Default is 0.",
},
{
// Not really deprecated (--codec has never been released), but without
@ -297,8 +329,7 @@ static const struct sc_option options[] = {
.argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation "
"(typically, portrait for a phone, landscape for a tablet). "
"Any --max-size value is computed on the cropped size.",
"(typically, portrait for a phone, landscape for a tablet).",
},
{
.shortopt = 'd',
@ -318,12 +349,10 @@ static const struct sc_option options[] = {
.argdesc = "id",
},
{
// deprecated
.longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying. "
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{
.longopt_id = OPT_DISPLAY_ID,
@ -442,6 +471,11 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.",
},
{
.longopt_id = OPT_LIST_APPS,
.longopt = "list-apps",
.text = "List Android apps installed on the device.",
},
{
.longopt_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras",
@ -463,18 +497,10 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.",
},
{
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation",
.argdesc = "value",
.optional_arg = true,
.text = "Lock capture video orientation to value.\n"
"Possible values are \"unlocked\", \"initial\" (locked to the "
"initial orientation), 0, 90, 180 and 270. The values "
"represent the clockwise rotation from the natural device "
"orientation, in degrees.\n"
"Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing "
"\"initial\".",
},
{
.shortopt = 'm',
@ -557,6 +583,20 @@ static const struct sc_option options[] = {
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
.longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.optional_arg = true,
.text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display "
"dimensions and DPI.\n"
"Examples:\n"
" --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display # main display size and density\n"
" --new-display=/240 # main display size and 240 dpi",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
@ -619,6 +659,11 @@ static const struct sc_option options[] = {
.longopt = "no-power-on",
.text = "Do not power on the device on start.",
},
{
.longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS,
.longopt = "no-vd-system-decorations",
.text = "Disable virtual display system decorations flag.",
},
{
.longopt_id = OPT_NO_VIDEO,
.longopt = "no-video",
@ -771,6 +816,13 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.longopt_id = OPT_SCREEN_OFF_TIMEOUT,
.longopt = "screen-off-timeout",
.argdesc = "seconds",
.text = "Set the screen off timeout while scrcpy is running (restore "
"the initial value on exit).",
},
{
.longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod",
@ -784,6 +836,20 @@ static const struct sc_option options[] = {
"shortcuts, pass \"lctrl,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
.longopt_id = OPT_START_APP,
.longopt = "start-app",
.argdesc = "name",
.text = "Start an Android app, by its exact package name.\n"
"Add a '?' prefix to select an app whose name starts with the "
"given name, case-insensitive (retrieving app names on the "
"device may take some time):\n"
" scrcpy --start-app=?firefox\n"
"Add a '+' prefix to force-stop before starting the app:\n"
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
"Both prefixes can be used, in that order:\n"
" scrcpy --start-app=+?firefox",
},
{
.shortopt = 't',
.longopt = "show-touches",
@ -851,8 +917,6 @@ static const struct sc_option options[] = {
.longopt = "v4l2-sink",
.argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n"
"It requires to lock the video orientation (see "
"--lock-video-orientation).\n"
"This feature is only available on Linux.",
},
{
@ -861,11 +925,20 @@ static const struct sc_option options[] = {
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n"
"This option is similar to --display-buffer, but specific to "
"This option is similar to --video-buffer, but specific to "
"V4L2 sink.\n"
"Default is 0 (no buffering).\n"
"This option is only available on Linux.",
},
{
.longopt_id = OPT_VIDEO_BUFFER,
.longopt = "video-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying "
"video frames.\n"
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
@ -977,6 +1050,10 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" },
.text = "Unpause display",
},
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{
.shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)",
@ -1072,7 +1149,11 @@ static const struct sc_shortcut shortcuts[] = {
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
.text = "Tilt vertically (slide with 2 fingers)",
},
{
.shortcuts = { "Ctrl+Shift+click-and-move" },
.text = "Tilt horizontally (slide with 2 fingers)",
},
{
.shortcuts = { "Drag & drop APK file" },
@ -1521,78 +1602,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
return true;
}
if (!strcmp(s, "90")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
if (!strcmp(s, "180")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "270")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "1")) {
LOGW("--lock-video-orientation=1 is deprecated, use "
"--lock-video-orientation=270 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "2")) {
LOGW("--lock-video-orientation=2 is deprecated, use "
"--lock-video-orientation=180 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "3")) {
LOGW("--lock-video-orientation=3 is deprecated, use "
"--lock-video-orientation=90 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
LOGE("Unsupported --lock-video-orientation value: %s (expected initial, "
"unlocked, 0, 90, 180 or 270).", s);
return false;
}
static bool
parse_rotation(const char *s, uint8_t *rotation) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
if (!ok) {
return false;
}
*rotation = (uint8_t) value;
return true;
}
static bool
parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) {
@ -1632,6 +1641,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false;
}
static bool
parse_capture_orientation(const char *s, enum sc_orientation *orientation,
enum sc_orientation_lock *lock) {
if (*s == '\0') {
LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, "
"flip0, flip90, flip180 or flip270, possibly prefixed by '@')");
return false;
}
// Lock the orientation by a leading '@'
if (s[0] == '@') {
// Consume '@'
++s;
if (*s == '\0') {
// Only '@': lock to the initial orientation (orientation is unused)
*lock = SC_ORIENTATION_LOCKED_INITIAL;
return true;
}
*lock = SC_ORIENTATION_LOCKED_VALUE;
} else {
*lock = SC_ORIENTATION_UNLOCKED;
}
return parse_orientation(s, orientation);
}
static bool
parse_window_position(const char *s, int16_t *position) {
// special value for "auto"
@ -2102,6 +2137,20 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_screen_off_timeout(const char *s, sc_tick *tick) {
long value;
// value in seconds, but must fit in 31 bits in milliseconds
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000,
"screen off timeout");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) {
@ -2227,8 +2276,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->crop = optarg;
break;
case OPT_DISPLAY:
LOGW("--display is deprecated, use --display-id instead.");
// fall through
LOGE("--display has been removed, use --display-id instead.");
return false;
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
@ -2292,8 +2341,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead.");
return false;
case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg,
&opts->lock_video_orientation)) {
LOGE("--lock-video-orientation has been removed, use "
"--capture-orientation instead.");
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
return false;
}
break;
@ -2311,8 +2365,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->control = false;
break;
case OPT_NO_DISPLAY:
LOGW("--no-display is deprecated, use --no-playback instead.");
// fall through
LOGE("--no-display has been removed, use --no-playback "
"instead.");
return false;
case 'N':
opts->video_playback = false;
opts->audio_playback = false;
@ -2398,32 +2453,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break;
case OPT_ROTATION:
LOGW("--rotation is deprecated, use --display-orientation "
"instead.");
uint8_t rotation;
if (!parse_rotation(optarg, &rotation)) {
LOGE("--rotation has been removed, use --orientation or "
"--capture-orientation instead.");
return false;
}
assert(rotation <= 3);
switch (rotation) {
case 0:
opts->display_orientation = SC_ORIENTATION_0;
break;
case 1:
// rotation 1 was 90° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_270;
break;
case 2:
opts->display_orientation = SC_ORIENTATION_180;
break;
case 3:
// rotation 3 was 270° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_90;
break;
}
break;
case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) {
return false;
@ -2484,23 +2516,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case OPT_FORWARD_ALL_CLICKS:
LOGW("--forward-all-clicks is deprecated, "
LOGE("--forward-all-clicks has been removed, "
"use --mouse-bind=++++ instead.");
opts->mouse_bindings = (struct sc_mouse_bindings) {
.pri = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
.sec = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
};
break;
return false;
case OPT_LEGACY_PASTE:
opts->legacy_paste = true;
break;
@ -2508,7 +2526,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->power_off_on_close = true;
break;
case OPT_DISPLAY_BUFFER:
if (!parse_buffering_time(optarg, &opts->display_buffer)) {
LOGE("--display-buffer has been removed, use --video-buffer "
"instead.");
return false;
case OPT_VIDEO_BUFFER:
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
return false;
}
break;
@ -2591,6 +2613,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_LIST_CAMERA_SIZES:
opts->list |= SC_OPTION_LIST_CAMERA_SIZES;
break;
case OPT_LIST_APPS:
opts->list |= SC_OPTION_LIST_APPS;
break;
case OPT_REQUIRE_AUDIO:
opts->require_audio = true;
break;
@ -2664,6 +2689,24 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_NEW_DISPLAY:
opts->new_display = optarg ? optarg : "";
break;
case OPT_START_APP:
opts->start_app = optarg;
break;
case OPT_SCREEN_OFF_TIMEOUT:
if (!parse_screen_off_timeout(optarg,
&opts->screen_off_timeout)) {
return false;
}
break;
case OPT_ANGLE:
opts->angle = optarg;
break;
case OPT_NO_VD_SYSTEM_DECORATIONS:
opts->vd_system_decorations = optarg;
break;
default:
// getopt prints the error message on stderr
return false;
@ -2758,13 +2801,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
// V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior,
// not an explicit request from the user.
@ -2772,7 +2808,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->v4l2_buffer && !opts->v4l2_device) {
LOGE("V4L2 buffer value without V4L2 sink\n");
LOGE("V4L2 buffer value without V4L2 sink");
return false;
}
#endif
@ -2791,8 +2827,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
} else if (!opts->video_playback) {
LOGI("No video mirroring, mouse mode switched to UHID");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
LOGI("No video mirroring, SDK mouse disabled");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
} else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
}
@ -2844,6 +2880,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->new_display) {
if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) {
LOGE("--new-display is only available with --video-source=display");
return false;
}
if (!opts->video) {
LOGE("--new-display is incompatible with --no-video");
return false;
}
}
if (otg) {
if (!opts->control) {
LOGE("--no-control is not allowed in OTG mode");
@ -2950,6 +2998,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->display_id != 0 && opts->new_display) {
LOGE("Cannot specify both --display-id and --new-display");
return false;
}
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
@ -3082,6 +3135,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("Cannot request power off on close if control is disabled");
return false;
}
if (opts->start_app) {
LOGE("Cannot start an Android app if control is disabled");
return false;
}
}
# ifdef _WIN32

View File

@ -22,9 +22,6 @@
#define MOTIONEVENT_ACTION_LABEL(value) \
ENUM_TO_LABEL(android_motionevent_action_labels, value)
#define SCREEN_POWER_MODE_LABEL(value) \
ENUM_TO_LABEL(screen_power_mode_labels, value)
static const char *const android_keyevent_action_labels[] = {
"down",
"up",
@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = {
"btn-release",
};
static const char *const screen_power_mode_labels[] = {
"off",
"doze",
"normal",
"doze-suspend",
"suspend",
};
static const char *const copy_key_labels[] = {
"none",
"copy",
@ -158,8 +147,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len;
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
buf[1] = msg->set_display_power.on;
return 2;
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write16be(&buf[1], msg->uhid_create.id);
@ -183,11 +172,16 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
sc_write16be(&buf[1], msg->uhid_destroy.id);
return 3;
case SC_CONTROL_MSG_TYPE_START_APP: {
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
return 1 + len;
}
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data
return 1;
default:
@ -264,9 +258,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
msg->set_clipboard.paste ? "paste" : "nopaste",
msg->set_clipboard.text);
break;
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
LOG_CMSG("power mode %s",
SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode));
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
LOG_CMSG("display power %s",
msg->set_display_power.on ? "on" : "off");
break;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
LOG_CMSG("expand notification panel");
@ -308,6 +302,12 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
LOG_CMSG("open hard keyboard settings");
break;
case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name);
break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break;
@ -333,6 +333,9 @@ sc_control_msg_destroy(struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
free(msg->set_clipboard.text);
break;
case SC_CONTROL_MSG_TYPE_START_APP:
free(msg->start_app.name);
break;
default:
// do nothing
break;

View File

@ -35,18 +35,14 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
};
enum sc_screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SC_SCREEN_POWER_MODE_OFF = 0,
SC_SCREEN_POWER_MODE_NORMAL = 2,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
enum sc_copy_key {
@ -94,8 +90,8 @@ struct sc_control_msg {
bool paste;
} set_clipboard;
struct {
enum sc_screen_power_mode mode;
} set_screen_power_mode;
bool on;
} set_display_power;
struct {
uint16_t id;
const char *name; // pointer to static data
@ -110,6 +106,9 @@ struct sc_control_msg {
struct {
uint16_t id;
} uhid_destroy;
struct {
char *name;
} start_app;
};
};

View File

@ -5,7 +5,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL_events.h>
#include <SDL2/SDL_events.h>
enum {
SC_EVENT_NEW_FRAME = SDL_USEREVENT,

View File

@ -9,6 +9,7 @@
#include "config.h"
#include "compat.h"
#include "util/env.h"
#include "util/file.h"
#include "util/log.h"
#include "util/str.h"
@ -19,35 +20,22 @@
static char *
get_icon_path(void) {
#ifdef __WINDOWS__
const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH");
#else
const char *icon_path_env = getenv("SCRCPY_ICON_PATH");
#endif
if (icon_path_env) {
char *icon_path = sc_get_env("SCRCPY_ICON_PATH");
if (icon_path) {
// if the envvar is set, use it
#ifdef __WINDOWS__
char *icon_path = sc_str_from_wchars(icon_path_env);
#else
char *icon_path = strdup(icon_path_env);
#endif
if (!icon_path) {
LOG_OOM();
return NULL;
}
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
return icon_path;
}
#ifndef PORTABLE
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
if (!icon_path) {
LOG_OOM();
return NULL;
}
#else
char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
if (!icon_path) {
LOGE("Could not get icon path");
return NULL;

View File

@ -5,53 +5,9 @@
#include "input_events.h"
#include "screen.h"
#include "shortcut_mod.h"
#include "util/log.h"
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(uint8_t shortcut_mod) {
uint16_t sdl_mod = 0;
if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (shortcut_mod & SC_SHORTCUT_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static bool
is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
// at least one shortcut mod pressed?
return sdl_mod & im->sdl_shortcut_mods;
}
static bool
is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
@ -73,7 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im,
im->legacy_paste = params->legacy_paste;
im->clipboard_autosync = params->clipboard_autosync;
im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods);
im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods);
im->vfinger_down = false;
im->vfinger_invert_x = false;
@ -247,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
}
static void
set_screen_power_mode(struct sc_input_manager *im,
enum sc_screen_power_mode mode) {
set_display_power(struct sc_input_manager *im, bool on) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = mode;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
msg.set_display_power.on = on;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
@ -329,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) {
}
}
static void
reset_video(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request reset video");
}
}
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) {
@ -346,7 +313,8 @@ sc_input_manager_process_text_input(struct sc_input_manager *im,
return;
}
if (is_shortcut_mod(im, SDL_GetModState())) {
if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods,
SDL_GetModState())) {
// A shortcut must never generate text events
return;
}
@ -413,8 +381,9 @@ sc_input_manager_process_key(struct sc_input_manager *im,
// press/release is a modifier key.
// The second condition is necessary to ignore the release of the modifier
// key (because in this case mod is 0).
bool is_shortcut = is_shortcut_mod(im, mod)
|| is_shortcut_key(im, sdl_keycode);
uint16_t mods = im->sdl_shortcut_mods;
bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod)
|| sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode);
if (down && !repeat) {
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
@ -457,10 +426,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
case SDLK_o:
if (control && !repeat && down && !paused) {
enum sc_screen_power_mode mode = shift
? SC_SCREEN_POWER_MODE_NORMAL
: SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(im, mode);
bool on = shift;
set_display_power(im, on);
}
return;
case SDLK_z:
@ -536,7 +503,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
case SDLK_f:
if (video && !shift && !repeat && down) {
sc_screen_switch_fullscreen(im->screen);
sc_screen_toggle_fullscreen(im->screen);
}
return;
case SDLK_w:
@ -566,9 +533,13 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_r:
if (control && !shift && !repeat && down && !paused) {
if (control && !repeat && down && !paused) {
if (shift) {
reset_video(im);
} else {
rotate_device(im);
}
}
return;
case SDLK_k:
if (control && !shift && !repeat && down && !paused
@ -836,7 +807,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
}
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) ||
(!down && im->vfinger_down));
bool use_finger = im->vfinger_down || change_vfinger;
@ -868,16 +839,28 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// In other words, the center of the rotation/scaling is the center of the
// screen.
//
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
// can be used instead of Ctrl. The "virtual finger" has a position
// To simulate a vertical tilt gesture (a vertical slide with two fingers),
// Shift can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
//
// To simulate a horizontal tilt gesture (a horizontal slide with two
// fingers), Ctrl+Shift can be used. The "virtual finger" has a position
// inverted with respect to the horizontal axis of symmetry in the middle
// of the screen. It is expected to be less frequently used, that's why the
// one-mod shortcuts are assigned to rotation and vertical tilt.
if (change_vfinger) {
struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y);
if (down) {
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
// Ctrl Shift invert_x invert_y
// ---- ----- ==> -------- --------
// 0 0 0 0 -
// 0 1 1 0 vertical tilt
// 1 0 1 1 rotate
// 1 1 0 1 horizontal tilt
im->vfinger_invert_x = ctrl_pressed ^ shift_pressed;
im->vfinger_invert_y = ctrl_pressed;
}
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,

View File

@ -45,6 +45,10 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
{SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT},
{SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT},
{SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT},
{SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT},
{SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT},
{SC_KEYCODE_LGUI, AKEYCODE_META_LEFT},
{SC_KEYCODE_RGUI, AKEYCODE_META_RIGHT},
};
// Numpad navigation keys.
@ -166,11 +170,7 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod,
return false;
}
if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) {
return false;
}
// if ALT and META are not pressed, also handle letters and space
// Handle letters and space
entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from);
if (entry) {
*to = entry->value;

123
app/src/mouse_capture.c Normal file
View File

@ -0,0 +1,123 @@
#include "mouse_capture.h"
#include "shortcut_mod.h"
#include "util/log.h"
void
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
uint8_t shortcut_mods) {
mc->window = window;
mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods);
mc->mouse_capture_key_pressed = SDLK_UNKNOWN;
}
static inline bool
sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) {
return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key);
}
bool
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
const SDL_Event *event) {
switch (event->type) {
case SDL_WINDOWEVENT:
if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
sc_mouse_capture_set_active(mc, false);
return true;
}
break;
case SDL_KEYDOWN: {
SDL_Keycode key = event->key.keysym.sym;
if (sc_mouse_capture_is_capture_key(mc, key)) {
if (!mc->mouse_capture_key_pressed) {
mc->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
mc->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return true;
}
break;
}
case SDL_KEYUP: {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = mc->mouse_capture_key_pressed;
mc->mouse_capture_key_pressed = 0;
if (sc_mouse_capture_is_capture_key(mc, key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_mouse_capture_toggle(mc);
}
// Mouse capture keys are never forwarded to the device
return true;
}
break;
}
case SDL_MOUSEWHEEL:
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
if (!sc_mouse_capture_is_active(mc)) {
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
// the event
return true;
}
break;
case SDL_MOUSEBUTTONUP:
if (!sc_mouse_capture_is_active(mc)) {
sc_mouse_capture_set_active(mc, true);
return true;
}
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
// Touch events are not compatible with relative mode
// (coordinates are not relative), so consume the event
return true;
}
return false;
}
void
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(mc->window, &x, &y);
SDL_GetWindowSize(mc->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(mc->window, w / 2, h / 2);
}
}
#else
(void) mc;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
bool
sc_mouse_capture_is_active(struct sc_mouse_capture *mc) {
(void) mc;
return SDL_GetRelativeMouseMode();
}
void
sc_mouse_capture_toggle(struct sc_mouse_capture *mc) {
bool new_value = !sc_mouse_capture_is_active(mc);
sc_mouse_capture_set_active(mc, new_value);
}

38
app/src/mouse_capture.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef SC_MOUSE_CAPTURE_H
#define SC_MOUSE_CAPTURE_H
#include "common.h"
#include <stdbool.h>
#include <SDL2/SDL.h>
struct sc_mouse_capture {
SDL_Window *window;
uint16_t sdl_mouse_capture_keys;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
SDL_Keycode mouse_capture_key_pressed;
};
void
sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window,
uint8_t shortcut_mods);
void
sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture);
bool
sc_mouse_capture_is_active(struct sc_mouse_capture *mc);
void
sc_mouse_capture_toggle(struct sc_mouse_capture *mc);
// Return true if it consumed the event
bool
sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
const SDL_Event *event);
#endif

View File

@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = {
.video_bit_rate = 0,
.audio_bit_rate = 0,
.max_fps = NULL,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.capture_orientation = SC_ORIENTATION_0,
.capture_orientation_lock = SC_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0,
.window_x = SC_WINDOW_POSITION_UNDEFINED,
@ -58,10 +59,11 @@ const struct scrcpy_options scrcpy_options_default = {
.window_width = 0,
.window_height = 0,
.display_id = 0,
.display_buffer = 0,
.video_buffer = 0,
.audio_buffer = -1, // depends on the audio format,
.audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0,
.screen_off_timeout = -1,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,
@ -103,6 +105,10 @@ const struct scrcpy_options scrcpy_options_default = {
.window = true,
.mouse_hover = true,
.audio_dup = false,
.new_display = NULL,
.start_app = NULL,
.angle = NULL,
.vd_system_decorations = true,
};
enum sc_orientation

View File

@ -84,6 +84,12 @@ enum sc_orientation { // v v v
SC_ORIENTATION_FLIP_270, // 1 1 1
};
enum sc_orientation_lock {
SC_ORIENTATION_UNLOCKED,
SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation
SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation
};
static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7));
@ -130,16 +136,6 @@ sc_orientation_get_name(enum sc_orientation orientation) {
}
}
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
SC_LOCK_VIDEO_ORIENTATION_270 = 1,
};
enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
@ -251,7 +247,9 @@ struct scrcpy_options {
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server
enum sc_lock_video_orientation lock_video_orientation;
const char *angle; // float to be parsed by the server
enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
enum sc_orientation display_orientation;
enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
@ -259,10 +257,11 @@ struct scrcpy_options {
uint16_t window_width;
uint16_t window_height;
uint32_t display_id;
sc_tick display_buffer;
sc_tick video_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
sc_tick time_limit;
sc_tick screen_off_timeout;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;
@ -304,10 +303,14 @@ struct scrcpy_options {
#define SC_OPTION_LIST_DISPLAYS 0x2
#define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8
#define SC_OPTION_LIST_APPS 0x10
uint8_t list;
bool window;
bool mouse_hover;
bool audio_dup;
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
const char *start_app;
bool vd_system_decorations;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -143,8 +143,14 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
return false;
}
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
char *file_url = sc_str_concat("file:", recorder->filename);
if (!file_url) {
avformat_free_context(recorder->ctx);
return false;
}
int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE);
free(file_url);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx);

View File

@ -53,7 +53,7 @@ struct scrcpy {
struct sc_decoder video_decoder;
struct sc_decoder audio_decoder;
struct sc_recorder recorder;
struct sc_delay_buffer display_buffer;
struct sc_delay_buffer video_buffer;
#ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink;
struct sc_delay_buffer v4l2_buffer;
@ -428,9 +428,13 @@ scrcpy(struct scrcpy_options *options) {
.video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.angle = options->angle,
.screen_off_timeout = options->screen_off_timeout,
.capture_orientation = options->capture_orientation,
.capture_orientation_lock = options->capture_orientation_lock,
.control = options->control,
.display_id = options->display_id,
.new_display = options->new_display,
.video = options->video,
.audio = options->audio,
.audio_dup = options->audio_dup,
@ -454,6 +458,7 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed,
.vd_system_decorations = options->vd_system_decorations,
.list = options->list,
};
@ -814,11 +819,11 @@ aoa_complete:
if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
if (options->video_buffer) {
sc_delay_buffer_init(&s->video_buffer,
options->video_buffer, true);
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink);
src = &s->video_buffer.frame_source;
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
@ -872,11 +877,11 @@ aoa_complete:
// everything is set up
if (options->control && options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
msg.set_display_power.on = false;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
LOGW("Could not request 'set display power'");
}
}
@ -906,6 +911,25 @@ aoa_complete:
init_sdl_gamepads();
}
if (options->control && options->start_app) {
assert(controller);
char *name = strdup(options->start_app);
if (!name) {
LOG_OOM();
goto end;
}
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_START_APP;
msg.start_app.name = name;
if (!sc_controller_push_msg(controller, &msg)) {
LOGW("Could not request start app '%s'", name);
free(name);
}
}
ret = event_loop(s);
terminate_event_loop();
LOGD("quit...");

View File

@ -162,47 +162,6 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
return screen->im.mp && screen->im.mp->relative_mode;
}
static void
sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(screen->window, &x, &y);
SDL_GetWindowSize(screen->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
}
}
#else
(void) screen;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
static inline bool
sc_screen_get_mouse_capture(struct sc_screen *screen) {
(void) screen;
return SDL_GetRelativeMouseMode();
}
static inline void
sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
(void) screen;
bool new_value = !sc_screen_get_mouse_capture(screen);
sc_screen_set_mouse_capture(screen, new_value);
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
@ -371,7 +330,6 @@ sc_screen_init(struct sc_screen *screen,
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->mouse_capture_key_pressed = 0;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
@ -486,6 +444,9 @@ sc_screen_init(struct sc_screen *screen,
sc_input_manager_init(&screen->im, &im_params);
// Initialize even if not used for simplicity
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (screen->video) {
SDL_AddEventWatch(event_watcher, screen);
@ -506,7 +467,7 @@ sc_screen_init(struct sc_screen *screen,
if (!screen->video && sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_screen_set_mouse_capture(screen, true);
sc_mouse_capture_set_active(&screen->mc, true);
}
return true;
@ -538,7 +499,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
SDL_SetWindowPosition(screen->window, x, y);
if (screen->req.fullscreen) {
sc_screen_switch_fullscreen(screen);
sc_screen_toggle_fullscreen(screen);
}
if (screen->req.start_fps_counter) {
@ -713,7 +674,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse on start
sc_screen_set_mouse_capture(screen, true);
sc_mouse_capture_set_active(&screen->mc, true);
}
}
@ -774,7 +735,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
}
void
sc_screen_switch_fullscreen(struct sc_screen *screen) {
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
assert(screen->video);
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
@ -837,15 +798,8 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
content_size.height);
}
static inline bool
sc_screen_is_mouse_capture_key(SDL_Keycode key) {
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
}
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
case SC_EVENT_SCREEN_INIT_SIZE: {
// The initial size is passed via screen->frame_size
@ -903,70 +857,15 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
if (relative_mode) {
sc_screen_set_mouse_capture(screen, false);
}
break;
}
return true;
case SDL_KEYDOWN:
if (relative_mode) {
SDL_Keycode key = event->key.keysym.sym;
if (sc_screen_is_mouse_capture_key(key)) {
if (!screen->mouse_capture_key_pressed) {
screen->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
screen->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return true;
}
}
break;
case SDL_KEYUP:
if (relative_mode) {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = screen->mouse_capture_key_pressed;
screen->mouse_capture_key_pressed = 0;
if (sc_screen_is_mouse_capture_key(key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_screen_toggle_mouse_capture(screen);
}
// Mouse capture keys are never forwarded to the device
return true;
}
}
break;
case SDL_MOUSEWHEEL:
case SDL_MOUSEMOTION:
case SDL_MOUSEBUTTONDOWN:
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
// Do not forward to input manager, the mouse will be captured
// on SDL_MOUSEBUTTONUP
return true;
}
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
if (relative_mode) {
// Touch events are not compatible with relative mode
// (coordinates are not relative)
return true;
}
break;
case SDL_MOUSEBUTTONUP:
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
sc_screen_set_mouse_capture(screen, true);
return true;
}
break;
}
sc_input_manager_handle_event(&screen->im, event);
return true;

View File

@ -13,6 +13,7 @@
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
#include "mouse_capture.h"
#include "opengl.h"
#include "options.h"
#include "trait/key_processor.h"
@ -30,6 +31,7 @@ struct sc_screen {
struct sc_display display;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
struct sc_fps_counter fps_counter;
@ -61,10 +63,6 @@ struct sc_screen {
bool maximized;
bool minimized;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
SDL_Keycode mouse_capture_key_pressed;
AVFrame *frame;
bool paused;
@ -126,9 +124,9 @@ sc_screen_destroy(struct sc_screen *screen);
void
sc_screen_hide_window(struct sc_screen *screen);
// switch the fullscreen mode
// toggle the fullscreen mode
void
sc_screen_switch_fullscreen(struct sc_screen *screen);
sc_screen_toggle_fullscreen(struct sc_screen *screen);
// resize window to optimal size (remove black borders)
void

View File

@ -9,6 +9,7 @@
#include "adb/adb.h"
#include "util/binary.h"
#include "util/env.h"
#include "util/file.h"
#include "util/log.h"
#include "util/net_intr.h"
@ -25,35 +26,22 @@
static char *
get_server_path(void) {
#ifdef __WINDOWS__
const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH");
#else
const char *server_path_env = getenv("SCRCPY_SERVER_PATH");
#endif
if (server_path_env) {
char *server_path = sc_get_env("SCRCPY_SERVER_PATH");
if (server_path) {
// if the envvar is set, use it
#ifdef __WINDOWS__
char *server_path = sc_str_from_wchars(server_path_env);
#else
char *server_path = strdup(server_path_env);
#endif
if (!server_path) {
LOG_OOM();
return NULL;
}
LOGD("Using SCRCPY_SERVER_PATH: %s", server_path);
return server_path;
}
#ifndef PORTABLE
LOGD("Using server: " SC_SERVER_PATH_DEFAULT);
char *server_path = strdup(SC_SERVER_PATH_DEFAULT);
server_path = strdup(SC_SERVER_PATH_DEFAULT);
if (!server_path) {
LOG_OOM();
return NULL;
}
#else
char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
server_path = sc_file_get_local_path(SC_SERVER_FILENAME);
if (!server_path) {
LOGE("Could not get local file path, "
"using " SC_SERVER_FILENAME " from current directory");
@ -66,56 +54,6 @@ get_server_path(void) {
return server_path;
}
static void
sc_server_params_destroy(struct sc_server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->req_serial);
free((char *) params->crop);
free((char *) params->video_codec_options);
free((char *) params->audio_codec_options);
free((char *) params->video_encoder);
free((char *) params->audio_encoder);
free((char *) params->tcpip_dst);
free((char *) params->camera_id);
free((char *) params->camera_ar);
}
static bool
sc_server_params_copy(struct sc_server_params *dst,
const struct sc_server_params *src) {
*dst = *src;
// The params reference user-allocated memory, so we must copy them to
// handle them from another thread
#define COPY(FIELD) do { \
dst->FIELD = NULL; \
if (src->FIELD) { \
dst->FIELD = strdup(src->FIELD); \
if (!dst->FIELD) { \
goto error; \
} \
} \
} while(0)
COPY(req_serial);
COPY(crop);
COPY(video_codec_options);
COPY(audio_codec_options);
COPY(video_encoder);
COPY(audio_encoder);
COPY(tcpip_dst);
COPY(camera_id);
COPY(camera_ar);
#undef COPY
return true;
error:
sc_server_params_destroy(dst);
return false;
}
static bool
push_server(struct sc_intr *intr, const char *serial) {
char *server_path = get_server_path();
@ -251,18 +189,31 @@ execute_server(struct sc_server *server,
cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER
uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial);
if (!sdk_version) {
LOGE("Could not determine SDK version");
return 0;
}
# define SERVER_DEBUGGER_PORT "5005"
cmd[count++] =
# ifdef SERVER_DEBUGGER_METHOD_NEW
/* Android 9 and above */
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y,"
"server=y,address="
# else
/* Android 8 and below */
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
# endif
const char *dbg;
if (sdk_version < 28) {
// Android < 9
dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
SERVER_DEBUGGER_PORT;
} else if (sdk_version < 30) {
// Android >= 9 && Android < 11
dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,"
"suspend=y,server=y,address=" SERVER_DEBUGGER_PORT;
} else {
// Android >= 11
// Contrary to the other methods, this does not suspend on start.
// <https://github.com/Genymobile/scrcpy/pull/5466>
dbg = "-XjdwpProvider:adbconnection";
}
cmd[count++] = dbg;
#endif
cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION;
@ -324,9 +275,21 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps);
}
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
ADD_PARAM("lock_video_orientation=%" PRIi8,
params->lock_video_orientation);
if (params->angle) {
VALIDATE_STRING(params->angle);
ADD_PARAM("angle=%s", params->angle);
}
if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED
|| params->capture_orientation != SC_ORIENTATION_0) {
if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) {
ADD_PARAM("capture_orientation=@");
} else {
const char *orient =
sc_orientation_get_name(params->capture_orientation);
bool locked =
params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED;
ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient);
}
}
if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true");
@ -370,6 +333,11 @@ execute_server(struct sc_server *server,
if (params->stay_awake) {
ADD_PARAM("stay_awake=true");
}
if (params->screen_off_timeout != -1) {
assert(params->screen_off_timeout >= 0);
uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout);
ADD_PARAM("screen_off_timeout=%" PRIu64, ms);
}
if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
@ -405,6 +373,13 @@ execute_server(struct sc_server *server,
// By default, power_on is true
ADD_PARAM("power_on=false");
}
if (params->new_display) {
VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", params->new_display);
}
if (!params->vd_system_decorations) {
ADD_PARAM("vd_system_decorations=false");
}
if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true");
}
@ -417,16 +392,23 @@ execute_server(struct sc_server *server,
if (params->list & SC_OPTION_LIST_CAMERA_SIZES) {
ADD_PARAM("list_camera_sizes=true");
}
if (params->list & SC_OPTION_LIST_APPS) {
ADD_PARAM("list_apps=true");
}
#undef ADD_PARAM
cmd[count++] = NULL;
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
SERVER_DEBUGGER_PORT "...");
// From the computer, run
// adb forward tcp:5005 tcp:5005
LOGI("Server debugger listening%s...",
sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : "");
// For Android < 11, from the computer:
// - run `adb forward tcp:5005 tcp:5005`
// For Android >= 11:
// - execute `adb jdwp` to get the jdwp port
// - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX)
//
// Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with:
// Host: localhost
@ -499,22 +481,25 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay,
bool
sc_server_init(struct sc_server *server, const struct sc_server_params *params,
const struct sc_server_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_server_params_copy(&server->params, params);
// The allocated data in params (const char *) must remain valid until the
// end of the program
server->params = *params;
bool ok = sc_adb_init();
if (!ok) {
LOG_OOM();
return false;
}
ok = sc_mutex_init(&server->mutex);
if (!ok) {
sc_server_params_destroy(&server->params);
sc_adb_destroy();
return false;
}
ok = sc_cond_init(&server->cond_stopped);
if (!ok) {
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
sc_adb_destroy();
return false;
}
@ -522,7 +507,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
if (!ok) {
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_server_params_destroy(&server->params);
sc_adb_destroy();
return false;
}
@ -1161,8 +1146,9 @@ sc_server_destroy(struct sc_server *server) {
free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped);
sc_mutex_destroy(&server->mutex);
sc_adb_destroy();
}

View File

@ -45,9 +45,13 @@ struct sc_server_params {
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server
int8_t lock_video_orientation;
const char *angle; // float to be parsed by the server
sc_tick screen_off_timeout;
enum sc_orientation capture_orientation;
enum sc_orientation_lock capture_orientation_lock;
bool control;
uint32_t display_id;
const char *new_display;
bool video;
bool audio;
bool audio_dup;
@ -65,6 +69,7 @@ struct sc_server_params {
bool power_on;
bool kill_adb_on_close;
bool camera_high_speed;
bool vd_system_decorations;
uint8_t list;
};

60
app/src/shortcut_mod.h Normal file
View File

@ -0,0 +1,60 @@
#ifndef SC_SHORTCUT_MOD_H
#define SC_SHORTCUT_MOD_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_keycode.h>
#include "options.h"
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
// input: OR of enum sc_shortcut_mod
// output: OR of SDL_Keymod
static inline uint16_t
sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) {
uint16_t sdl_mod = 0;
if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) {
sdl_mod |= KMOD_RCTRL;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LALT) {
sdl_mod |= KMOD_LALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RALT) {
sdl_mod |= KMOD_RALT;
}
if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) {
sdl_mod |= KMOD_LGUI;
}
if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) {
sdl_mod |= KMOD_RGUI;
}
return sdl_mod;
}
static inline bool
sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) {
// sdl_shortcut_mods must be within the mask
assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK));
// at least one shortcut mod pressed?
return sdl_mod & sdl_shortcut_mods;
}
static inline bool
sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods,
SDL_Keycode keycode) {
return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
#endif

View File

@ -6,6 +6,9 @@
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#ifdef __APPLE__
# include <mach-o/dyld.h> // for _NSGetExecutablePath()
#endif
#include "util/log.h"
@ -60,11 +63,22 @@ sc_file_get_executable_path(void) {
}
buf[len] = '\0';
return strdup(buf);
#else
// in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform
// (it's useful to have a working version on Linux for debugging though)
#elif defined(__APPLE__)
char buf[PATH_MAX];
uint32_t bufsize = PATH_MAX;
if (_NSGetExecutablePath(buf, &bufsize) != 0) {
LOGE("Executable path buffer too small; need %u bytes", bufsize);
return NULL;
}
return realpath(buf, NULL);
#else
// "_" is often used to store the full path of the command being executed
char *path = getenv("_");
if (!path) {
LOGE("Could not determine executable path");
return NULL;
}
return strdup(path);
#endif
}

View File

@ -1,6 +1,8 @@
#ifndef SC_AOA_HID_H
#define SC_AOA_HID_H
#include "common.h"
#include <stdint.h>
#include <stdbool.h>

View File

@ -95,9 +95,14 @@ scrcpy_otg(struct scrcpy_options *options) {
// On Windows, only one process could open a USB device
// <https://github.com/Genymobile/scrcpy/issues/2773>
LOGI("Killing adb server (if any)...");
if (sc_adb_init()) {
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
// uninterruptible (intr == NULL), but in practice it's very quick
sc_adb_kill_server(NULL, flags);
sc_adb_destroy();
} else {
LOGW("Could not call adb executable, adb server not killed");
}
#endif
static const struct sc_usb_callbacks cbs = {
@ -185,6 +190,7 @@ 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,
};
ok = sc_screen_otg_init(&s->screen_otg, &params);

View File

@ -4,47 +4,6 @@
#include "options.h"
#include "util/log.h"
static void
sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) {
#ifdef __APPLE__
// Workaround for SDL bug on macOS:
// <https://github.com/libsdl-org/SDL/issues/5340>
if (capture) {
int mouse_x, mouse_y;
SDL_GetGlobalMouseState(&mouse_x, &mouse_y);
int x, y, w, h;
SDL_GetWindowPosition(screen->window, &x, &y);
SDL_GetWindowSize(screen->window, &w, &h);
bool outside_window = mouse_x < x || mouse_x >= x + w
|| mouse_y < y || mouse_y >= y + h;
if (outside_window) {
SDL_WarpMouseInWindow(screen->window, w / 2, h / 2);
}
}
#else
(void) screen;
#endif
if (SDL_SetRelativeMouseMode(capture)) {
LOGE("Could not set relative mouse mode to %s: %s",
capture ? "true" : "false", SDL_GetError());
}
}
static inline bool
sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) {
(void) screen;
return SDL_GetRelativeMouseMode();
}
static inline void
sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) {
(void) screen;
bool new_value = !sc_screen_otg_get_mouse_capture(screen);
sc_screen_otg_set_mouse_capture(screen, new_value);
}
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
SDL_RenderClear(screen->renderer);
@ -61,8 +20,6 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
screen->mouse = params->mouse;
screen->gamepad = params->gamepad;
screen->mouse_capture_key_pressed = 0;
const char *title = params->window_title;
assert(title);
@ -113,9 +70,11 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
LOGW("Could not load icon");
}
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
if (screen->mouse) {
// Capture mouse on start
sc_screen_otg_set_mouse_capture(screen, true);
sc_mouse_capture_set_active(&screen->mc, true);
}
return true;
@ -137,11 +96,6 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) {
SDL_DestroyWindow(screen->window);
}
static inline bool
sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) {
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
}
static void
sc_screen_otg_process_key(struct sc_screen_otg *screen,
const SDL_KeyboardEvent *event) {
@ -298,80 +252,46 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
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_WINDOWEVENT:
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
sc_screen_otg_render(screen);
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
if (screen->mouse) {
sc_screen_otg_set_mouse_capture(screen, false);
}
break;
}
return;
case SDL_KEYDOWN:
if (screen->mouse) {
SDL_Keycode key = event->key.keysym.sym;
if (sc_screen_otg_is_mouse_capture_key(key)) {
if (!screen->mouse_capture_key_pressed) {
screen->mouse_capture_key_pressed = key;
} else {
// Another mouse capture key has been pressed, cancel
// mouse (un)capture
screen->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return;
}
}
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_KEYUP:
if (screen->mouse) {
SDL_Keycode key = event->key.keysym.sym;
SDL_Keycode cap = screen->mouse_capture_key_pressed;
screen->mouse_capture_key_pressed = 0;
if (sc_screen_otg_is_mouse_capture_key(key)) {
if (key == cap) {
// A mouse capture key has been pressed then released:
// toggle the capture mouse mode
sc_screen_otg_toggle_mouse_capture(screen);
}
// Mouse capture keys are never forwarded to the device
return;
}
}
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_MOUSEMOTION:
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
if (screen->mouse) {
sc_screen_otg_process_mouse_motion(screen, &event->motion);
}
break;
case SDL_MOUSEBUTTONDOWN:
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_MOUSEBUTTONUP:
if (screen->mouse) {
if (sc_screen_otg_get_mouse_capture(screen)) {
sc_screen_otg_process_mouse_button(screen, &event->button);
} else {
sc_screen_otg_set_mouse_capture(screen, true);
}
}
break;
case SDL_MOUSEWHEEL:
if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) {
if (screen->mouse) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;

View File

@ -8,6 +8,7 @@
#include "keyboard_aoa.h"
#include "mouse_aoa.h"
#include "mouse_capture.h"
#include "gamepad_aoa.h"
struct sc_screen_otg {
@ -19,8 +20,7 @@ struct sc_screen_otg {
SDL_Renderer *renderer;
SDL_Texture *texture;
// See equivalent mechanism in screen.h
SDL_Keycode mouse_capture_key_pressed;
struct sc_mouse_capture mc;
};
struct sc_screen_otg_params {
@ -35,6 +35,7 @@ struct sc_screen_otg_params {
uint16_t window_width;
uint16_t window_height;
bool window_borderless;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
bool

29
app/src/util/env.c Normal file
View File

@ -0,0 +1,29 @@
#include "env.h"
#include <stdlib.h>
#include <string.h>
#include "util/str.h"
char *
sc_get_env(const char *varname) {
#ifdef _WIN32
wchar_t *w_varname = sc_str_to_wchars(varname);
if (!w_varname) {
return NULL;
}
const wchar_t *value = _wgetenv(w_varname);
free(w_varname);
if (!value) {
return NULL;
}
return sc_str_from_wchars(value);
#else
const char *value = getenv(varname);
if (!value) {
return NULL;
}
return strdup(value);
#endif
}

12
app/src/util/env.h Normal file
View File

@ -0,0 +1,12 @@
#ifndef SC_ENV_H
#define SC_ENV_H
#include "common.h"
// Return the value of the environment variable (may be NULL).
//
// The returned value must be freed by the caller.
char *
sc_get_env(const char *varname);
#endif

View File

@ -9,8 +9,6 @@
#ifdef _WIN32
# include <ws2tcpip.h>
typedef int socklen_t;
typedef SOCKET sc_raw_socket;
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
#else
# include <sys/types.h>
# include <sys/socket.h>
@ -23,8 +21,6 @@
typedef struct sockaddr_in SOCKADDR_IN;
typedef struct sockaddr SOCKADDR;
typedef struct in_addr IN_ADDR;
typedef int sc_raw_socket;
# define SC_RAW_SOCKET_NONE -1
#endif
bool
@ -47,17 +43,26 @@ net_cleanup(void) {
#endif
}
static inline bool
sc_raw_socket_close(sc_raw_socket raw_sock) {
#ifndef _WIN32
return !close(raw_sock);
#else
return !closesocket(raw_sock);
#endif
}
static inline sc_socket
wrap(sc_raw_socket sock) {
#ifdef _WIN32
if (sock == INVALID_SOCKET) {
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
if (sock == SC_RAW_SOCKET_NONE) {
return SC_SOCKET_NONE;
}
struct sc_socket_windows *socket = malloc(sizeof(*socket));
struct sc_socket_wrapper *socket = malloc(sizeof(*socket));
if (!socket) {
LOG_OOM();
closesocket(sock);
sc_raw_socket_close(sock);
return SC_SOCKET_NONE;
}
@ -72,9 +77,9 @@ wrap(sc_raw_socket sock) {
static inline sc_raw_socket
unwrap(sc_socket socket) {
#ifdef _WIN32
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
if (socket == SC_SOCKET_NONE) {
return INVALID_SOCKET;
return SC_RAW_SOCKET_NONE;
}
return socket->socket;
@ -83,17 +88,6 @@ unwrap(sc_socket socket) {
#endif
}
#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning
static inline bool
sc_raw_socket_close(sc_raw_socket raw_sock) {
#ifndef _WIN32
return !close(raw_sock);
#else
return !closesocket(raw_sock);
#endif
}
#endif
#ifndef HAVE_SOCK_CLOEXEC
// If SOCK_CLOEXEC does not exist, the flag must be set manually once the
// socket is created
@ -248,9 +242,9 @@ net_interrupt(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket);
#ifdef _WIN32
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
if (!atomic_flag_test_and_set(&socket->closed)) {
return !closesocket(raw_sock);
return sc_raw_socket_close(raw_sock);
}
return true;
#else
@ -262,15 +256,15 @@ bool
net_close(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket);
#ifdef _WIN32
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
bool ret = true;
if (!atomic_flag_test_and_set(&socket->closed)) {
ret = !closesocket(raw_sock);
ret = sc_raw_socket_close(raw_sock);
}
free(socket);
return ret;
#else
return !close(raw_sock);
return sc_raw_socket_close(raw_sock);
#endif
}

View File

@ -7,21 +7,36 @@
#include <stdint.h>
#ifdef _WIN32
# include <winsock2.h>
typedef SOCKET sc_raw_socket;
# define SC_RAW_SOCKET_NONE INVALID_SOCKET
#else // not _WIN32
# include <sys/socket.h>
typedef int sc_raw_socket;
# define SC_RAW_SOCKET_NONE -1
#endif
#if defined(_WIN32) || defined(__APPLE__)
// On Windows and macOS, shutdown() does not interrupt accept() or read()
// calls, so net_interrupt() must call close() instead, and net_close() must
// behave accordingly.
// This causes a small race condition (once the socket is closed, its
// handle becomes invalid and may in theory be reassigned before another
// thread calls accept() or read()), but it is deemed acceptable as a
// workaround.
# define SC_SOCKET_CLOSE_ON_INTERRUPT
#endif
#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT
# include <stdatomic.h>
# define SC_SOCKET_NONE NULL
typedef struct sc_socket_windows {
SOCKET socket;
typedef struct sc_socket_wrapper {
sc_raw_socket socket;
atomic_flag closed;
} *sc_socket;
#else // not _WIN32
# include <sys/socket.h>
#else
# define SC_SOCKET_NONE -1
typedef int sc_socket;
typedef sc_raw_socket sc_socket;
#endif
#define IPV4_LOCALHOST 0x7F000001

View File

@ -64,6 +64,26 @@ sc_str_quote(const char *src) {
return quoted;
}
char *
sc_str_concat(const char *start, const char *end) {
assert(start);
assert(end);
size_t start_len = strlen(start);
size_t end_len = strlen(end);
char *result = malloc(start_len + end_len + 1);
if (!result) {
LOG_OOM();
return NULL;
}
memcpy(result, start, start_len);
memcpy(result + start_len, end, end_len + 1);
return result;
}
bool
sc_str_parse_integer(const char *s, long *out) {
char *endptr;

View File

@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
char *
sc_str_quote(const char *src);
/**
* Concat two strings
*
* Return a new allocated string, contanining the concatenation of the two
* input strings.
*/
char *
sc_str_concat(const char *start, const char *end);
/**
* Parse `s` as an integer into `out`
*

View File

@ -10,14 +10,14 @@ typedef int64_t sc_tick;
#define SC_TICK_FREQ 1000000 // microsecond
// To be adapted if SC_TICK_FREQ changes
#define SC_TICK_TO_NS(tick) ((tick) * 1000)
#define SC_TICK_TO_US(tick) (tick)
#define SC_TICK_TO_MS(tick) ((tick) / 1000)
#define SC_TICK_TO_SEC(tick) ((tick) / 1000000)
#define SC_TICK_FROM_NS(ns) ((ns) / 1000)
#define SC_TICK_FROM_US(us) (us)
#define SC_TICK_FROM_MS(ms) ((ms) * 1000)
#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000)
#define SC_TICK_TO_NS(tick) ((sc_tick) (tick) * 1000)
#define SC_TICK_TO_US(tick) ((sc_tick) tick)
#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000)
#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000)
#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000)
#define SC_TICK_FROM_US(us) ((sc_tick) us)
#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000)
#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000)
sc_tick
sc_tick_now(void);

View File

@ -62,6 +62,7 @@ void
sc_timeout_stop(struct sc_timeout *timeout) {
sc_mutex_lock(&timeout->mutex);
timeout->stopped = true;
sc_cond_signal(&timeout->cond);
sc_mutex_unlock(&timeout->mutex);
}

View File

@ -51,7 +51,6 @@ static void test_options(void) {
"--fullscreen",
"--max-fps", "30",
"--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236",
@ -80,7 +79,6 @@ static void test_options(void) {
assert(opts->fullscreen);
assert(!strcmp(opts->max_fps, "30"));
assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies"));

View File

@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_set_screen_power_mode(void) {
static void test_serialize_set_display_power(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
.set_screen_power_mode = {
.mode = SC_SCREEN_POWER_MODE_NORMAL,
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
.set_display_power = {
.on = true,
},
};
@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) {
assert(size == 2);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x02, // SC_SCREEN_POWER_MODE_NORMAL
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
0x01, // true
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_reset_video(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
@ -423,11 +438,12 @@ int main(int argc, char *argv[]) {
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_clipboard_long();
test_serialize_set_screen_power_mode();
test_serialize_set_display_power();
test_serialize_rotate_device();
test_serialize_uhid_create();
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0;
}

View File

@ -141,6 +141,16 @@ static void test_quote(void) {
free(out);
}
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
// contains the concat
assert(!strcmp("my-prefix:2024:11", out));
free(out);
}
static void test_utf8_truncate(void) {
const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide
@ -389,6 +399,7 @@ int main(int argc, char *argv[]) {
test_join_truncated_before_sep();
test_join_truncated_after_sep();
test_quote();
test_concat();
test_utf8_truncate();
test_parse_integer();
test_parse_integers();

View File

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

View File

@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to
avoid glitches and smooth the playback:
```
scrcpy --display-buffer=200 --audio-buffer=200
scrcpy --video-buffer=200 --audio-buffer=200
```
It is also possible to configure another audio buffer (the audio output buffer),

View File

@ -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 meson gcc make
sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v2.7`][direct-scrcpy-server]
<sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub>
- [`scrcpy-server-v3.0`][direct-scrcpy-server]
<sub>SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View File

@ -23,14 +23,20 @@ To control the device without mirroring:
scrcpy --no-video --no-audio
```
By default, mouse mode is switched to UHID if video mirroring is disabled (a
relative mouse mode is required).
By default, the mouse is disabled when video playback is turned off.
To control the device using a relative mouse, enable UHID mouse mode:
```bash
scrcpy --no-video --no-audio --mouse=uhid
scrcpy --no-video --no-audio -M # short version
```
To also use a UHID keyboard, set it explicitly:
```bash
scrcpy --no-video --no-audio --keyboard=uhid
scrcpy --no-video --no-audio -K # short version
scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid
scrcpy --no-video --no-audio -MK # short version
```
To use AOA instead (over USB only):
@ -94,14 +100,18 @@ the content (if supported by the app) relative to the center of the screen.
https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
To simulate a vertical tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f
Similarly, to simulate a horizontal tilt gesture:
<kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move-left-or-right_.
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. When pressing
<kbd>Ctrl</kbd> the _x_ and _y_ coordinates are inverted. Using <kbd>Shift</kbd>
only inverts _x_.
only inverts _x_, whereas using <kbd>Ctrl</kbd>+<kbd>Shift</kbd> only inverts
_y_.
This only works for the default mouse mode (`--mouse=sdk`).

View File

@ -21,9 +21,9 @@ the client and on the server.
If video is enabled, then the server sends a raw video stream (H.264 by default)
of the device screen, with some additional headers for each packet. The client
decodes the video frames, and displays them as soon as possible, without
buffering (unless `--display-buffer=delay` is specified) to minimize latency.
The client is not aware of the device rotation (which is handled by the server),
it just knows the dimensions of the video frames it receives.
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The
client is not aware of the device rotation (which is handled by the server), it
just knows the dimensions of the video frames it receives.
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
by default) of the device audio output (or the microphone if
@ -461,26 +461,30 @@ meson setup x -Dserver_debugger=true
meson configure x -Dserver_debugger=true
```
If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition:
Then recompile, and run scrcpy.
```bash
meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
```
Then recompile.
When you start scrcpy, it will start a debugger on port 5005 on the device.
For Android < 11, it will start a debugger on port 5005 on the device and wait:
Redirect that port to the computer:
```bash
adb forward tcp:5005 tcp:5005
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on
`+`, _Remote_, and fill the form:
For Android >= 11, first find the listening port:
```bash
adb jdwp
# press Ctrl+C to interrupt
```
Then redirect the resulting PID:
```bash
adb forward tcp:5005 jdwp:XXXX # replace XXXX
```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click
on `+`, _Remote_, and fill the form:
- Host: `localhost`
- Port: `5005`

View File

@ -18,6 +18,21 @@ The initial state is restored when _scrcpy_ is closed.
If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior).
This changes the value of [`stay_on_while_plugged_in`], setting which can be
changed manually:
[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN
```bash
# get the current show_touches value
adb shell settings get global stay_on_while_plugged_in
# enable for AC/USB/wireless chargers
adb shell settings put global stay_on_while_plugged_in 7
# disable
adb shell settings put global stay_on_while_plugged_in 0
```
## Turn screen off
@ -46,6 +61,40 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version
```
Since Android 15, it is possible to change this setting manually:
```
# turn screen off (0 for main display)
adb shell cmd display power-off 0
# turn screen on
adb shell cmd display power-on 0
```
## Screen off timeout
The Android screen automatically turns off after some delay.
To change this delay while scrcpy is running:
```bash
scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes)
```
The initial value is restored on exit.
It is possible to change this setting manually:
```bash
# get the current screen_off_timeout value
adb shell settings get system screen_off_timeout
# set a new value (in milliseconds)
adb shell settings put system screen_off_timeout 30000
```
Note that the Android value is in milliseconds, but the scrcpy command line
argument is in seconds.
## Show touches
@ -62,6 +111,16 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device).
It is possible to change this setting manually:
```bash
# get the current show_touches value
adb shell settings get system show_touches
# enable show_touches
adb shell settings put system show_touches 1
# disable show_touches
adb shell settings put system show_touches 0
```
## Power off on close
@ -78,3 +137,48 @@ By default, on start, the device is powered on. To prevent this behavior:
```bash
scrcpy --no-power-on
```
## Start Android app
To list the Android apps installed on the device:
```bash
scrcpy --list-apps
```
An app, selected by its package name, can be launched on start:
```
scrcpy --start-app=org.mozilla.firefox
```
This feature can be used to run an app in a [virtual
display](virtual_display.md):
```
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
```
The app can be optionally forced-stop before being started, by adding a `+`
prefix:
```
scrcpy --start-app=+org.mozilla.firefox
```
For convenience, it is also possible to select an app by its name, by adding a
`?` prefix:
```
scrcpy --start-app=?firefox
```
But retrieving app names may take some time (sometimes several seconds), so
passing the package name is recommended.
The `+` and `?` prefixes can be combined (in that order):
```
scrcpy --start-app=+?firefox
```

View File

@ -2,6 +2,23 @@
## Install
### From the official release
Download a static build of the [latest release]:
- [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64)
<sub>SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz
and extract it.
_Static builds of scrcpy for Linux are still experimental._
### From your package manager
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
Scrcpy is packaged in several distributions and package managers:
@ -13,10 +30,10 @@ Scrcpy is packaged in several distributions and package managers:
- Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions))
### Latest version
However, the packaged version is not always the latest release. To install the
latest release from `master`, follow this simplified process.
### From an install script
To install the latest release from `master`, follow this simplified process.
First, you need to install the required packages:

View File

@ -2,6 +2,23 @@
## Install
### From the official release
Download a static build of the [latest release]:
- [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64)
<sub>SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz
and extract it.
_Static builds of scrcpy for macOS are still experimental._
### From a package manager
Scrcpy is available in [Homebrew]:
```bash
@ -13,7 +30,7 @@ brew install scrcpy
You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash
brew install android-platform-tools
brew install --cask android-platform-tools
```
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:

View File

@ -34,9 +34,9 @@ Two modes allow to simulate a physical HID mouse on the device.
In these modes, the computer mouse is "captured": the mouse pointer disappears
from the computer and appears on the Android device instead.
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
(disable or enable) the mouse capture. Use one of them to give the control of
the mouse back to the computer.
The [shortcut mod](shortcuts.md) (either <kbd>Alt</kbd> or <kbd>Super</kbd> by
default) toggle (disable or enable) the mouse capture. Use one of them to give
the control of the mouse back to the computer.
### UHID

View File

@ -30,6 +30,7 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Reset video capture/encoding | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>r</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
@ -53,7 +54,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Open keyboard settings (HID keyboard only) | <kbd>MOD</kbd>+<kbd>k</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Tilt vertically (slide with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| 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)

View File

@ -27,6 +27,9 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
If encoding fails, scrcpy automatically tries again with a lower definition
(unless `--no-downsize-on-error` is enabled).
For camera mirroring, the `--max-size` value is used to select the camera source
size instead (among the available resolutions).
## Bit rate
@ -93,7 +96,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one:
```bash
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc
```
@ -103,24 +106,45 @@ The orientation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation
- `--capture-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the
recording.
- `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md).
To lock the mirroring orientation (on the capture side):
To capture the video with a specific orientation:
```bash
scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --lock-video-orientation=90 # 90° clockwise
scrcpy --lock-video-orientation=180 # 180°
scrcpy --lock-video-orientation=270 # 270° clockwise
scrcpy --capture-orientation=0
scrcpy --capture-orientation=90 # 90° clockwise
scrcpy --capture-orientation=180 # 180°
scrcpy --capture-orientation=270 # 270° clockwise
scrcpy --capture-orientation=flip0 # hflip
scrcpy --capture-orientation=flip90 # hflip + 90° clockwise
scrcpy --capture-orientation=flip180 # hflip + 180°
scrcpy --capture-orientation=flip270 # hflip + 270° clockwise
```
To orient the video (on the rendering side):
The capture orientation can be locked by using `@`, so that a physical device
rotation does not change the captured video orientation:
```bash
scrcpy --capture-orientation=@ # locked to the initial orientation
scrcpy --capture-orientation=@0 # locked to 0°
scrcpy --capture-orientation=@90 # locked to 90° clockwise
scrcpy --capture-orientation=@180 # locked to 180°
scrcpy --capture-orientation=@270 # locked to 270° clockwise
scrcpy --capture-orientation=@flip0 # locked to hflip
scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise
scrcpy --capture-orientation=@flip180 # locked to hflip + 180°
scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise
```
The capture orientation transform is applied after `--crop`, but before
`--angle`.
To orient the video (on the client side):
```bash
scrcpy --orientation=0
@ -141,6 +165,19 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
values are allowed when recording.
## Angle
To rotate the video content by a custom angle (in degrees, clockwise):
```
scrcpy --angle=23
```
The center of rotation is the center of the visible area.
This transformation is applied after `--crop` and `--capture-orientation`.
## Crop
The device screen may be cropped to mirror only part of the screen.
@ -154,7 +191,11 @@ scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
The values are expressed in the device natural orientation (portrait for a
phone, landscape for a tablet).
If `--max-size` is also specified, resizing is applied after cropping.
Cropping is performed before `--capture-orientation` and `--angle`.
For display mirroring, `--max-size` is applied after cropping. For camera,
`--max-size` is applied first (because it selects the source size rather than
resizing the content).
## Display
@ -175,6 +216,8 @@ scrcpy --list-displays
A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only).
It is also possible to create a [virtual display](virtual_display.md).
## Buffering
@ -189,15 +232,15 @@ The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash
scrcpy --display-buffer=50 # add 50ms buffering for display
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
scrcpy --video-buffer=50 # add 50ms buffering for video playback
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
```
They can be applied simultaneously:
```bash
scrcpy --display-buffer=50 --v4l2-buffer=300
scrcpy --video-buffer=50 --v4l2-buffer=300
```

35
doc/virtual_display.md Normal file
View File

@ -0,0 +1,35 @@
# Virtual display
## New display
To mirror a new virtual display instead of the device screen:
```bash
scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display # use the main display size and density
scrcpy --new-display=/240 # use the main display size and 240 dpi
```
## Start app
On some devices, a launcher is available in the virtual display.
When no launcher is available, the virtual display is empty. In that case, you
must [start an Android app](device.md#start-android-app).
For example:
```bash
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
```
## System decorations
By default, virtual display system decorations are enabled. But some devices
might display a broken UI;
Use `--no-vd-system-decorations` to disable it.
Note that if no app is started, no content will be rendered, so no video frame
will be produced at all.

View File

@ -2,27 +2,32 @@
## Install
### From the official release
Download the [latest release]:
- [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit)
<sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub>
- [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit)
<sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub>
- [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit)
<sub>SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be`</sub>
- [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit)
<sub>SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip
and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]:
### From a package manager
From [Chocolatey]:
```bash
choco install scrcpy
choco install adb # if you don't have it yet
```
or [Scoop]:
From [Scoop]:
```bash
@ -30,7 +35,6 @@ scoop install scrcpy
scoop install adb # if you don't have it yet
```
[Winget]: https://github.com/microsoft/winget-cli
[Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh

View File

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

View File

@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7
PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0
PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '2.7',
version: '3.0',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',

View File

@ -2,7 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('static', type: 'boolean', value: false, description: 'Use static dependencies')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')
option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')

View File

@ -1,131 +0,0 @@
# This makefile provides recipes to build a "portable" version of scrcpy for
# Windows.
#
# Here, "portable" means that the client and server binaries are expected to be
# anywhere, but in the same directory, instead of well-defined separate
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server).
#
# In particular, this implies to change the location from where the client push
# the server to the device.
.PHONY: default clean \
test \
build-server \
prepare-deps \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
release
GRADLE ?= ./gradlew
TEST_BUILD_DIR := build-test
SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION)
release: clean test build-server zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
cd "$(RELEASE_DIR)" && \
sha256sum "scrcpy-server-$(VERSION)" \
"scrcpy-win32-$(VERSION).zip" \
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
@echo "Release generated in $(RELEASE_DIR)/"
clean:
$(GRADLE) clean
rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
test:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
$(GRADLE) -p server check
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
@app/deps/adb.sh win32
@app/deps/sdl.sh win32
@app/deps/ffmpeg.sh win32
@app/deps/libusb.sh win32
prepare-deps-win64:
@app/deps/adb.sh win64
@app/deps/sdl.sh win64
@app/deps/ffmpeg.sh win64
@app/deps/libusb.sh win64
build-win32: prepare-deps-win32
rm -rf "$(WIN32_BUILD_DIR)"
mkdir -p "$(WIN32_BUILD_DIR)/local"
meson setup "$(WIN32_BUILD_DIR)" \
--pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \
-Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \
-Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \
--cross-file=cross_win32.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps-win64
rm -rf "$(WIN64_BUILD_DIR)"
mkdir -p "$(WIN64_BUILD_DIR)/local"
meson setup "$(WIN64_BUILD_DIR)" \
--pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \
-Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \
-Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \
--cross-file=cross_win64.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN64_BUILD_DIR)"
dist-win32: build-server build-win32
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"

View File

@ -1,2 +0,0 @@
#!/bin/bash
make -f release.mk

2
release/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/work
/output

5
release/build_common Normal file
View File

@ -0,0 +1,5 @@
# This file must be sourced from the release scripts directory
WORK_DIR="$PWD/work"
OUTPUT_DIR="$PWD/output"
VERSION="${VERSION:-$(git describe --tags --always)}"

42
release/build_linux.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 1 ]]
then
echo "Syntax: $0 <arch>" >&2
exit 1
fi
ARCH="$1"
LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH"
app/deps/adb_linux.sh
app/deps/sdl.sh linux native static
app/deps/ffmpeg.sh linux native static
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"
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 \
--strip \
-Db_lto=true \
-Dcompile_server=false \
-Dportable=true \
-Dstatic=true
ninja -C "$LINUX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$LINUX_BUILD_DIR/dist"
cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/"
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

42
release/build_macos.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 1 ]]
then
echo "Syntax: $0 <arch>" >&2
exit 1
fi
ARCH="$1"
MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH"
app/deps/adb_macos.sh
app/deps/sdl.sh macos native static
app/deps/ffmpeg.sh macos native static
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"
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 \
--strip \
-Db_lto=true \
-Dcompile_server=false \
-Dportable=true \
-Dstatic=true
ninja -C "$MACOS_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$MACOS_BUILD_DIR/dist"
cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/"
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"

14
release/build_server.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
GRADLE="${GRADLE:-./gradlew}"
SERVER_BUILD_DIR="$WORK_DIR/build-server"
rm -rf "$SERVER_BUILD_DIR"
"$GRADLE" -p server assembleRelease
mkdir -p "$SERVER_BUILD_DIR/server"
cp server/build/outputs/apk/release/server-release-unsigned.apk \
"$SERVER_BUILD_DIR/server/scrcpy-server"

52
release/build_windows.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
set -ex
case "$1" in
32)
WINXX=win32
;;
64)
WINXX=win64
;;
*)
echo "ERROR: $0 must be called with one argument: 32 or 64" >&2
exit 1
;;
esac
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX"
app/deps/adb_windows.sh
app/deps/sdl.sh $WINXX cross shared
app/deps/ffmpeg.sh $WINXX cross shared
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"
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 \
--buildtype=release \
--strip \
-Db_lto=true \
-Dcompile_server=false \
-Dportable=true
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/"
cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/"

14
release/generate_checksums.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd "$OUTPUT_DIR"
sha256sum "scrcpy-server-$VERSION" \
"scrcpy-linux-x86_64-$VERSION.tar.gz" \
"scrcpy-win32-$VERSION.zip" \
"scrcpy-win64-$VERSION.zip" \
"scrcpy-macos-aarch64-$VERSION.tar.gz" \
"scrcpy-macos-x86_64-$VERSION.tar.gz" \
| tee SHA256SUMS.txt
echo "Release checksums generated in $PWD/SHA256SUMS.txt"

52
release/package_client.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
if [[ $# != 2 ]]
then
# <target_name>: for example win64
# <format>: zip or tar.gz
echo "Syntax: $0 <target> <format>" >&2
exit 1
fi
FORMAT=$2
if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]]
then
echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2
exit 1
fi
BUILD_DIR="$WORK_DIR/build-$1"
ARCHIVE_DIR="$BUILD_DIR/release-archive"
TARGET_DIRNAME="scrcpy-$1-$VERSION"
rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME"
mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME"
cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/"
cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/"
mkdir -p "$OUTPUT_DIR"
cd "$ARCHIVE_DIR"
rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT"
case "$FORMAT" in
zip)
zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME"
;;
tar.gz)
tar cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME"
;;
*)
echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2
exit 1
esac
rm -rf "$TARGET_DIRNAME"
cd -
echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'"

10
release/package_server.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
OUTPUT_DIR="$PWD/output"
. build_common
cd .. # root project dir
mkdir -p "$OUTPUT_DIR"
cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION"
echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'"

24
release/release.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
# To customize the version name:
# VERSION=myversion ./release.sh
set -e
cd "$(dirname ${BASH_SOURCE[0]})"
rm -rf output
./test_server.sh
./test_client.sh
./build_server.sh
./build_windows.sh 32
./build_windows.sh 64
./build_linux.sh x86_64
./package_server.sh
./package_client.sh win32 zip
./package_client.sh win64 zip
./package_client.sh linux-x86_64 tar.gz
./generate_checksums.sh
echo "Release generated in $PWD/output"

12
release/test_client.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
TEST_BUILD_DIR="$WORK_DIR/build-test"
rm -rf "$TEST_BUILD_DIR"
meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \
-Db_sanitize=address,undefined
ninja -C "$TEST_BUILD_DIR" test

9
release/test_server.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
GRADLE="${GRADLE:-./gradlew}"
"$GRADLE" -p server check

View File

@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
android {
namespace 'com.genymobile.scrcpy'
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 34
versionCode 20700
versionName "2.7"
targetSdkVersion 35
versionCode 30000
versionName "3.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@ -12,10 +12,11 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.7
SCRCPY_VERSION_NAME=3.0
PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0}
PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM"
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -23,7 +24,8 @@ CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
ANDROID_JAR="$PLATFORM_TOOLS/android.jar"
ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM"
@ -45,16 +47,24 @@ EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \
android/view/IDisplayWindowListener.aidl
# Fake sources to expose hidden Android types to the project
FAKE_SRC=( \
android/content/*java \
)
SRC=( \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/opengl/*.java \
com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \
@ -68,10 +78,11 @@ done
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
${FAKE_SRC[@]} \
${SRC[@]}
echo "Dexing..."

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import android.graphics.Rect;
import android.content.res.Configuration;
import java.util.List;
/**
* Interface to listen for changes to display window-containers.
*
* This differs from DisplayManager's DisplayListener in a couple ways:
* - onDisplayAdded is always called after the display is actually added to the WM hierarchy.
* This corresponds to the DisplayContent and not the raw Dislay from DisplayManager.
* - onDisplayConfigurationChanged is called for all configuration changes, not just changes
* to displayinfo (eg. windowing-mode).
*
*/
oneway interface IDisplayWindowListener {
/**
* Called when a new display is added to the WM hierarchy. The existing display ids are returned
* when this listener is registered with WM via {@link #registerDisplayWindowListener}.
*/
void onDisplayAdded(int displayId);
/**
* Called when a display's window-container configuration has changed.
*/
void onDisplayConfigurationChanged(int displayId, in Configuration newConfig);
/**
* Called when a display is removed from the hierarchy.
*/
void onDisplayRemoved(int displayId);
/**
* Called when fixed rotation is started on a display.
*/
void onFixedRotationStarted(int displayId, int newRotation);
/**
* Called when the previous fixed rotation on a display is finished.
*/
void onFixedRotationFinished(int displayId);
/**
* Called when the keep clear ares on a display have changed.
*/
void onKeepClearAreasChanged(int displayId, in List<Rect> restricted, in List<Rect> unrestricted);
}

View File

@ -0,0 +1,5 @@
package android.content;
public interface IContentProvider {
// android.content.IContentProvider is hidden, this is a fake one to expose the type to the project
}

View File

@ -0,0 +1,32 @@
package com.genymobile.scrcpy;
import android.os.Build;
/**
* Android version code constants, done right.
* <p/>
* <a href="https://apilevels.com/">API levels</a>
*/
public final class AndroidVersions {
private AndroidVersions() {
// not instantiable
}
public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP;
public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1;
public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M;
public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N;
public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1;
public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O;
public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1;
public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P;
public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q;
public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R;
public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S;
public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2;
public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU;
public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM;
}

View File

@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@ -16,59 +18,132 @@ import java.io.OutputStream;
*/
public final class CleanUp {
private static final int MSG_TYPE_MASK = 0b11;
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
// Dynamic options
private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0;
private int pendingChanges;
private boolean pendingRestoreDisplayPower;
private static final int MSG_PARAM_SHIFT = 2;
private Thread thread;
private final OutputStream out;
public CleanUp(OutputStream out) {
this.out = out;
private CleanUp(Options options) {
thread = new Thread(() -> runCleanUp(options), "cleanup");
thread.start();
}
public static CleanUp configure(int displayId) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)};
public static CleanUp start(Options options) {
return new CleanUp(options);
}
public void interrupt() {
thread.interrupt();
}
public void join() throws InterruptedException {
thread.join();
}
private void runCleanUp(Options options) {
boolean disableShowTouches = false;
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
disableShowTouches = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
int restoreStayOn = -1;
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
int currentStayOn = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentStayOn != stayOn) {
restoreStayOn = currentStayOn;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
int restoreScreenOffTimeout = -1;
int screenOffTimeout = options.getScreenOffTimeout();
if (screenOffTimeout != -1) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout));
try {
int currentScreenOffTimeout = Integer.parseInt(oldValue);
// Restore only if the current value is different
if (currentScreenOffTimeout != screenOffTimeout) {
restoreScreenOffTimeout = currentScreenOffTimeout;
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"screen_off_timeout\"", e);
}
}
boolean powerOffScreen = options.getPowerOffScreenOnClose();
int displayId = options.getDisplayId();
try {
run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout);
} catch (InterruptedException e) {
// ignore
} catch (IOException e) {
Ln.e("Clean up I/O exception", e);
}
}
private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout)
throws IOException, InterruptedException {
String[] cmd = {
"app_process",
"/",
CleanUp.class.getName(),
String.valueOf(displayId),
String.valueOf(restoreStayOn),
String.valueOf(disableShowTouches),
String.valueOf(powerOffScreen),
String.valueOf(restoreScreenOffTimeout),
};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start();
return new CleanUp(process.getOutputStream());
}
OutputStream out = process.getOutputStream();
private boolean sendMessage(int type, int param) {
assert (type & ~MSG_TYPE_MASK) == 0;
int msg = type | param << MSG_PARAM_SHIFT;
try {
out.write(msg);
while (true) {
int localPendingChanges;
boolean localPendingRestoreDisplayPower;
synchronized (this) {
while (pendingChanges == 0) {
wait();
}
localPendingChanges = pendingChanges;
localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
pendingChanges = 0;
}
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
out.write(localPendingRestoreDisplayPower ? 1 : 0);
out.flush();
return true;
} catch (IOException e) {
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e);
return false;
}
}
}
public boolean setRestoreStayOn(int restoreValue) {
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
assert restoreValue >= -1 && restoreValue <= 7;
return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111);
}
public boolean setDisableShowTouches(boolean disableOnExit) {
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
}
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
}
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0);
public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
pendingRestoreDisplayPower = restoreDisplayPower;
pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
notify();
}
public static void unlinkSelf() {
@ -83,35 +158,21 @@ public final class CleanUp {
unlinkSelf();
int displayId = Integer.parseInt(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean disableShowTouches = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int restoreScreenOffTimeout = Integer.parseInt(args[4]);
int restoreStayOn = -1;
boolean disableShowTouches = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
// Dynamic option
boolean restoreDisplayPower = false;
try {
// Wait for the server to die
int msg;
while ((msg = System.in.read()) != -1) {
int type = msg & MSG_TYPE_MASK;
int param = msg >> MSG_PARAM_SHIFT;
switch (type) {
case MSG_TYPE_RESTORE_STAY_ON:
restoreStayOn = param > 7 ? -1 : param;
break;
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
disableShowTouches = param != 0;
break;
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
restoreNormalPowerMode = param != 0;
break;
case MSG_TYPE_POWER_OFF_SCREEN:
powerOffScreen = param != 0;
break;
default:
Ln.w("Unexpected msg type: " + type);
break;
}
// Only restore display power
assert msg == 0 || msg == 1;
restoreDisplayPower = msg != 0;
}
} catch (IOException e) {
// Expected when the server is dead
@ -137,13 +198,24 @@ public final class CleanUp {
}
}
if (Device.isScreenOn()) {
if (restoreScreenOffTimeout != -1) {
Ln.i("Restoring \"screen off timeout\"");
try {
Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout));
} catch (SettingsException e) {
Ln.e("Could not restore \"screen_off_timeout\"", e);
}
}
// Change the power of the main display when mirroring a virtual display
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
if (Device.isScreenOn(targetDisplayId)) {
if (powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(displayId);
} else if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
Device.powerOffScreen(targetDisplayId);
} else if (restoreDisplayPower) {
Ln.i("Restoring display power");
Device.setDisplayPower(targetDisplayId, true);
}
}

View File

@ -1,10 +1,14 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
@ -18,6 +22,38 @@ public final class FakeContext extends ContextWrapper {
return INSTANCE;
}
private final ContentResolver contentResolver = new ContentResolver(this) {
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireProvider(Context c, String name) {
return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder());
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings({"unused", "ProtectedMemberInFinalClass"})
// @Override (but super-class method not visible)
protected IContentProvider acquireUnstableProvider(Context c, String name) {
return null;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public boolean releaseUnstableProvider(IContentProvider icp) {
return false;
}
@SuppressWarnings("unused")
// @Override (but super-class method not visible)
public void unstableProviderDied(IContentProvider icp) {
// ignore
}
};
private FakeContext() {
super(Workarounds.getSystemContext());
}
@ -32,7 +68,7 @@ public final class FakeContext extends ContextWrapper {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@TargetApi(AndroidVersions.API_31_ANDROID_12)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
@ -50,4 +86,9 @@ public final class FakeContext extends ContextWrapper {
public Context getApplicationContext() {
return this;
}
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
}

View File

@ -2,6 +2,9 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln;
@ -11,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect;
import android.util.Pair;
import java.util.List;
import java.util.Locale;
@ -30,7 +34,7 @@ public class Options {
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private float maxFps;
private int lockVideoOrientation = -1;
private float angle;
private boolean tunnelForward;
private Rect crop;
private boolean control = true;
@ -43,6 +47,7 @@ public class Options {
private boolean cameraHighSpeed;
private boolean showTouches;
private boolean stayAwake;
private int screenOffTimeout = -1;
private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions;
@ -54,10 +59,17 @@ public class Options {
private boolean cleanup = true;
private boolean powerOn = true;
private NewDisplay newDisplay;
private boolean vdSystemDecorations = true;
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
private Orientation captureOrientation = Orientation.Orient0;
private boolean listEncoders;
private boolean listDisplays;
private boolean listCameras;
private boolean listCameraSizes;
private boolean listApps;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size
@ -117,8 +129,8 @@ public class Options {
return maxFps;
}
public int getLockVideoOrientation() {
return lockVideoOrientation;
public float getAngle() {
return angle;
}
public boolean isTunnelForward() {
@ -169,6 +181,10 @@ public class Options {
return stayAwake;
}
public int getScreenOffTimeout() {
return screenOffTimeout;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
@ -205,8 +221,24 @@ public class Options {
return powerOn;
}
public NewDisplay getNewDisplay() {
return newDisplay;
}
public Orientation getCaptureOrientation() {
return captureOrientation;
}
public Orientation.Lock getCaptureOrientationLock() {
return captureOrientationLock;
}
public boolean getVDSystemDecorations() {
return vdSystemDecorations;
}
public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes;
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
}
public boolean getListEncoders() {
@ -225,6 +257,10 @@ public class Options {
return listCameraSizes;
}
public boolean getListApps() {
return listApps;
}
public boolean getSendDeviceMeta() {
return sendDeviceMeta;
}
@ -323,8 +359,8 @@ public class Options {
case "max_fps":
options.maxFps = parseFloat("max_fps", value);
break;
case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value);
case "angle":
options.angle = parseFloat("angle", value);
break;
case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value);
@ -346,6 +382,12 @@ public class Options {
case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value);
break;
case "screen_off_timeout":
options.screenOffTimeout = Integer.parseInt(value);
if (options.screenOffTimeout < -1) {
throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout);
}
break;
case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value);
break;
@ -388,6 +430,9 @@ public class Options {
case "list_camera_sizes":
options.listCameraSizes = Boolean.parseBoolean(value);
break;
case "list_apps":
options.listApps = Boolean.parseBoolean(value);
break;
case "camera_id":
if (!value.isEmpty()) {
options.cameraId = value;
@ -418,6 +463,17 @@ public class Options {
case "camera_high_speed":
options.cameraHighSpeed = Boolean.parseBoolean(value);
break;
case "new_display":
options.newDisplay = parseNewDisplay(value);
break;
case "vd_system_decorations":
options.vdSystemDecorations = Boolean.parseBoolean(value);
break;
case "capture_orientation":
Pair<Orientation.Lock, Orientation> pair = parseCaptureOrientation(value);
options.captureOrientationLock = pair.first;
options.captureOrientation = pair.second;
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;
@ -445,6 +501,11 @@ public class Options {
}
}
if (options.newDisplay != null) {
assert options.displayId == 0 : "Must not set both displayId and newDisplay";
options.displayId = Device.DISPLAY_ID_NONE;
}
return options;
}
@ -475,6 +536,9 @@ public class Options {
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Invalid non-positive size dimension: \"" + size + "\"");
}
return new Size(width, height);
}
@ -501,4 +565,57 @@ public class Options {
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
}
}
private static NewDisplay parseNewDisplay(String newDisplay) {
// Possible inputs:
// - "" (empty string)
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
if (newDisplay.isEmpty()) {
return new NewDisplay();
}
String[] tokens = newDisplay.split("/");
Size size;
if (!tokens[0].isEmpty()) {
size = parseSize(tokens[0]);
} else {
size = null;
}
int dpi;
if (tokens.length >= 2) {
dpi = Integer.parseInt(tokens[1]);
if (dpi <= 0) {
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]);
}
} else {
dpi = 0;
}
return new NewDisplay(size, dpi);
}
private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {
if (value.isEmpty()) {
throw new IllegalArgumentException("Empty capture orientation string");
}
Orientation.Lock lock;
if (value.charAt(0) == '@') {
// Consume '@'
value = value.substring(1);
if (value.isEmpty()) {
// Only '@': lock to the initial orientation (orientation is unused)
return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0);
}
lock = Orientation.Lock.LockedValue;
} else {
lock = Orientation.Lock.Unlocked;
}
return Pair.create(lock, Orientation.getByName(value));
}
}

View File

@ -9,22 +9,21 @@ import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
import java.io.File;
@ -75,63 +74,21 @@ public final class Server {
// not instantiable
}
private static void initAndCleanUp(Options options, CleanUp cleanUp) {
// This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once
// and for all, they cannot be changed from another thread)
if (options.getShowTouches()) {
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
if (!"1".equals(oldValue)) {
if (!cleanUp.setDisableShowTouches(true)) {
Ln.e("Could not disable show touch on exit");
}
}
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
int restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn != stayOn) {
// Restore only if the current value is different
if (!cleanUp.setRestoreStayOn(restoreStayOn)) {
Ln.e("Could not restore stay on on exit");
}
}
} catch (NumberFormatException e) {
// ignore
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
if (options.getPowerOffScreenOnClose()) {
if (!cleanUp.setPowerOffScreen(true)) {
Ln.e("Could not power off screen on exit");
}
}
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) {
Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
}
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) {
Ln.e("New virtual display is not supported before Android 10");
throw new ConfigurationException("New virtual display is not supported");
}
CleanUp cleanUp = null;
Thread initThread = null;
if (options.getCleanup()) {
cleanUp = CleanUp.configure(options.getDisplayId());
initThread = startInitThread(options, cleanUp);
cleanUp = CleanUp.start(options);
}
int scid = options.getScid();
@ -140,9 +97,6 @@ public final class Server {
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
boolean camera = video && options.getVideoSource() == VideoSource.CAMERA;
final Device device = camera ? null : new Device(options);
Workarounds.apply();
@ -154,13 +108,11 @@ public final class Server {
connection.sendDeviceMeta(Device.getDeviceName());
}
Controller controller = null;
if (control) {
ControlChannel controlChannel = connection.getControlChannel();
Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
device.setClipboardListener(text -> {
DeviceMessage msg = DeviceMessage.createClipboard(text);
controller.getSender().send(msg);
});
controller = new Controller(controlChannel, cleanUp, options);
asyncProcessors.add(controller);
}
@ -179,8 +131,7 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
}
asyncProcessors.add(audioRecorder);
}
@ -190,14 +141,22 @@ public final class Server {
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
surfaceCapture = new ScreenCapture(device);
NewDisplay newDisplay = options.getNewDisplay();
if (newDisplay != null) {
surfaceCapture = new NewDisplayCapture(controller, options);
} else {
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, options);
}
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
} else {
surfaceCapture = new CameraCapture(options);
}
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options);
asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
}
Completion completion = new Completion(asyncProcessors.size());
@ -209,22 +168,25 @@ public final class Server {
completion.await();
} finally {
if (initThread != null) {
initThread.interrupt();
if (cleanUp != null) {
cleanUp.interrupt();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop();
}
OpenGLRunner.quit(); // quit the OpenGL thread, if any
connection.shutdown();
try {
if (initThread != null) {
initThread.join();
if (cleanUp != null) {
cleanUp.join();
}
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join();
}
OpenGLRunner.join();
} catch (InterruptedException e) {
// ignore
}
@ -233,12 +195,6 @@ public final class Server {
}
}
private static Thread startInitThread(final Options options, final CleanUp cleanUp) {
Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup");
thread.start();
return thread;
}
public static void main(String... args) {
int status = 0;
try {
@ -282,6 +238,11 @@ public final class Server {
Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
if (options.getListApps()) {
Workarounds.apply();
Ln.i("Processing Android apps... (this may take some time)");
Ln.i(LogUtils.buildAppListMessage());
}
// Just print the requested data, do not mirror
return;
}

View File

@ -52,7 +52,7 @@ public final class Workarounds {
}
public static void apply() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
@ -155,7 +155,7 @@ public final class Workarounds {
}
}
@TargetApi(Build.VERSION_CODES.R)
@TargetApi(AndroidVersions.API_30_ANDROID_11)
@SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException {
@ -226,7 +226,7 @@ public final class Workarounds {
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
int initResult;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) {
// private native final int native_setup(Object audiorecord_this,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
@ -252,7 +252,7 @@ public final class Workarounds {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) {
// private native int native_setup(Object audiorecordThis,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln;
@ -45,11 +46,11 @@ public class AudioDirectCapture implements AudioCapture {
}
}
@TargetApi(Build.VERSION_CODES.M)
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord(int audioSource) {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
@ -117,7 +118,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException();
}
@ -125,7 +126,7 @@ public class AudioDirectCapture implements AudioCapture {
@Override
public void start() throws AudioCaptureException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) {
startWorkaroundAndroid11();
try {
tryStartRecording(5, 100);
@ -146,7 +147,7 @@ public class AudioDirectCapture implements AudioCapture {
}
@Override
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}

View File

@ -1,14 +1,16 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Streamer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
@ -66,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended;
public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) {
this.capture = capture;
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
this.bitRate = options.getAudioBitRate();
this.codecOptions = options.getAudioCodecOptions();
this.encoderName = options.getAudioEncoder();
}
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
@ -93,7 +95,7 @@ public final class AudioEncoder implements AsyncProcessor {
return format;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@ -175,9 +177,9 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@TargetApi(Build.VERSION_CODES.M)
@TargetApi(AndroidVersions.API_23_ANDROID_6_0)
private void encode() throws IOException, ConfigurationException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
@ -287,7 +289,13 @@ public final class AudioEncoder implements AsyncProcessor {
if (encoderName != null) {
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName);
String mimeType = Codec.getMimeType(mediaCodec);
if (!codec.getMimeType().equals(mimeType)) {
Ln.e("Audio encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")");
throw new ConfigurationException("Incorrect encoder type: " + encoderName);
}
return mediaCodec;
} catch (IllegalArgumentException e) {
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
@ -308,7 +316,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
private final class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
@ -108,7 +109,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) {
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException();
}
@ -130,7 +131,7 @@ public final class AudioPlaybackCapture implements AudioCapture {
}
@Override
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}

View File

@ -1,9 +1,10 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.os.Build;
@ -24,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;

View File

@ -1,12 +1,12 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer;
@ -26,7 +26,7 @@ public class AudioRecordReader {
this.recorder = recorder;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(AndroidVersions.API_24_ANDROID_7_0)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) {

Some files were not shown because too many files have changed in this diff Show More