Compare commits

...

97 Commits

Author SHA1 Message Date
1ddf289d82 Enable close-on-interrupt for macOS
This behavior is also necessary on macOS.

TODO ref 5536
2024-11-27 10:19:34 +01:00
bf7c7d8038 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.

TODO ref 5536
2024-11-27 10:19:26 +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
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
102 changed files with 3967 additions and 1282 deletions

View File

@ -6,11 +6,15 @@ on:
name: name:
description: 'Version name (default is ref name)' description: 'Version name (default is ref name)'
env:
# $VERSION is used by release scripts
VERSION: ${{ github.event.inputs.name || github.ref_name }}
jobs: jobs:
build-scrcpy-server: test-scrcpy-server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
GRADLE: gradle # use native gradle instead of ./gradlew in release.mk GRADLE: gradle # use native gradle instead of ./gradlew in scripts
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -22,16 +26,45 @@ jobs:
java-version: '17' java-version: '17'
- name: Test scrcpy-server - name: Test scrcpy-server
run: make -f release.mk test-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 scrcpy-server - name: Build scrcpy-server
run: make -f release.mk build-server run: release/build_server.sh
- name: Upload scrcpy-server artifact - name: Upload scrcpy-server artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: scrcpy-server name: scrcpy-server
path: build-server/server/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 scrcpy-server without gradle
run: server/build_without_gradle.sh
test-client: test-client:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -44,15 +77,42 @@ jobs:
sudo apt update sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
- name: Build
run: |
meson setup d -Db_sanitize=address,undefined
- name: Test - name: Test
run: release/test_client.sh
build-linux:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: | run: |
meson test -Cd sudo apt update
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
libv4l-dev
- name: Build linux
run: release/build_linux.sh
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-linux
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload build-linux artifact
uses: actions/upload-artifact@v4
with:
name: build-linux-intermediate
path: release/work/build-linux/dist-tar/
build-win32: build-win32:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -71,14 +131,22 @@ jobs:
- name: Workaround for old meson version run by Github Actions - name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
- name: Build scrcpy win32 - name: Build win32
run: make -f release.mk build-win32 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 build-win32 artifact - name: Upload build-win32 artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: build-win32-intermediate name: build-win32-intermediate
path: build-win32/dist/ path: release/work/build-win32/dist-tar/
build-win64: build-win64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -97,24 +165,56 @@ jobs:
- name: Workaround for old meson version run by Github Actions - name: Workaround for old meson version run by Github Actions
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
- name: Build scrcpy win64 - name: Build win64
run: make -f release.mk build-win64 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 build-win64 artifact - name: Upload build-win64 artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: build-win64-intermediate name: build-win64-intermediate
path: build-win64/dist/ path: release/work/build-win64/dist-tar/
package: build-macos:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: |
brew install meson ninja nasm libiconv zlib automake autoconf \
libtool
- name: Build macOS
run: release/build_macos.sh
# upload-artifact does not preserve permissions
- name: Tar
run: |
cd release/work/build-macos
mkdir dist-tar
cd dist-tar
tar -C .. -cvf dist.tar.gz dist/
- name: Upload build-macos artifact
uses: actions/upload-artifact@v4
with:
name: build-macos-intermediate
path: release/work/build-macos/dist-tar/
package-linux:
needs: needs:
- build-scrcpy-server - build-scrcpy-server
- build-win32 - build-linux
- build-win64
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
# $VERSION is used by release.mk
VERSION: ${{ github.event.inputs.name || github.ref_name }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -123,25 +223,187 @@ jobs:
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: scrcpy-server name: scrcpy-server
path: build-server/server/ path: release/work/build-server/server/
- name: Download build-linux
uses: actions/download-artifact@v4
with:
name: build-linux-intermediate
path: release/work/build-linux/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-linux
tar xf dist-tar/dist.tar.gz
- name: Package linux
run: release/package_client.sh linux tar.gz
- name: Upload linux release
uses: actions/upload-artifact@v4
with:
name: release-linux
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 - name: Download build-win32
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: build-win32-intermediate name: build-win32-intermediate
path: build-win32/dist/ 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 win32
run: release/package_client.sh win32 zip
- name: Upload win32 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 - name: Download build-win64
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: build-win64-intermediate name: build-win64-intermediate
path: build-win64/dist/ path: release/work/build-win64/dist-tar/
- name: Package # upload-artifact does not preserve permissions
run: make -f release.mk package - name: Detar
run: |
cd release/work/build-win64
tar xf dist-tar/dist.tar.gz
- name: Package win64
run: release/package_client.sh win64 zip
- name: Upload win64 release
uses: actions/upload-artifact@v4
with:
name: release-win64
path: release/output
package-macos:
needs:
- build-scrcpy-server
- build-macos
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-intermediate
path: release/work/build-macos/dist-tar/
# upload-artifact does not preserve permissions
- name: Detar
run: |
cd release/work/build-macos
tar xf dist-tar/dist.tar.gz
- name: Package macos
run: release/package_client.sh macos tar.gz
- name: Upload macos release
uses: actions/upload-artifact@v4
with:
name: release-macos
path: release/output/
release:
needs:
- build-scrcpy-server
- package-linux
- package-win32
- package-win64
- package-macos
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
uses: actions/download-artifact@v4
with:
name: release-linux
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
uses: actions/download-artifact@v4
with:
name: release-macos
path: release/output/
- name: Package server
run: release/package_server.sh
- name: Generate checksums
run: release/generate_checksums.sh
- name: Upload release artifact - name: Upload release artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: scrcpy-release-${{ env.VERSION }} name: scrcpy-release-${{ env.VERSION }}
path: 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 source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v2.7) # scrcpy (v3.0)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -74,7 +74,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
## Get the app ## Get the app
- [Linux](doc/linux.md) - [Linux](doc/linux.md)
- [Windows](doc/windows.md) - [Windows](doc/windows.md) (read [how to run](doc/windows.md#run))
- [macOS](doc/macos.md) - [macOS](doc/macos.md)
@ -141,7 +141,7 @@ documented in the following pages:
- [Device](doc/device.md) - [Device](doc/device.md)
- [Window](doc/window.md) - [Window](doc/window.md)
- [Recording](doc/recording.md) - [Recording](doc/recording.md)
- [Virtual display](doc/virtual_displays.md) - [Virtual display](doc/virtual_display.md)
- [Tunnels](doc/tunnels.md) - [Tunnels](doc/tunnels.md)
- [OTG](doc/otg.md) - [OTG](doc/otg.md)
- [Camera](doc/camera.md) - [Camera](doc/camera.md)
@ -181,6 +181,7 @@ to your problem immediately.
You can also use: You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) - 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) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View File

@ -2,6 +2,7 @@ _scrcpy() {
local cur prev words cword local cur prev words cword
local opts=" local opts="
--always-on-top --always-on-top
--angle
--audio-bit-rate= --audio-bit-rate=
--audio-buffer= --audio-buffer=
--audio-codec= --audio-codec=
@ -17,10 +18,10 @@ _scrcpy() {
--camera-fps= --camera-fps=
--camera-high-speed --camera-high-speed
--camera-size= --camera-size=
--capture-orientation=
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
--display-buffer=
--display-id= --display-id=
--display-orientation= --display-orientation=
-e --select-tcpip -e --select-tcpip
@ -38,8 +39,6 @@ _scrcpy() {
--list-cameras --list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation
--lock-video-orientation=
-m --max-size= -m --max-size=
-M -M
--max-fps= --max-fps=
@ -58,6 +57,7 @@ _scrcpy() {
--no-mipmaps --no-mipmaps
--no-mouse-hover --no-mouse-hover
--no-power-on --no-power-on
--no-vd-system-decorations
--no-video --no-video
--no-video-playback --no-video-playback
--orientation= --orientation=
@ -78,6 +78,7 @@ _scrcpy() {
--rotation= --rotation=
-s --serial= -s --serial=
-S --turn-screen-off -S --turn-screen-off
--screen-off-timeout=
--shortcut-mod= --shortcut-mod=
--start-app= --start-app=
-t --show-touches -t --show-touches
@ -90,6 +91,7 @@ _scrcpy() {
--v4l2-sink= --v4l2-sink=
-v --version -v --version
-V --verbosity= -V --verbosity=
--video-buffer=
--video-codec= --video-codec=
--video-codec-options= --video-codec-options=
--video-encoder= --video-encoder=
@ -137,6 +139,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return 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) --orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
@ -145,10 +151,6 @@ _scrcpy() {
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
return
;;
--pause-on-exit) --pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return return
@ -191,9 +193,9 @@ _scrcpy() {
|--camera-size \ |--camera-size \
|--crop \ |--crop \
|--display-id \ |--display-id \
|--display-buffer \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \
|--new-display \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \ |--rotation \
@ -201,6 +203,7 @@ _scrcpy() {
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \
|--v4l2-sink \ |--v4l2-sink \
|--video-buffer \
|--video-codec-options \ |--video-codec-options \
|--video-encoder \ |--video-encoder \
|--tcpip \ |--tcpip \

View File

@ -0,0 +1,6 @@
#!/bin/bash
cd "$(dirname ${BASH_SOURCE[0]})"
export ADB="${ADB:-./adb}"
export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}"
export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}"
./scrcpy_bin "$@"

View File

@ -9,6 +9,7 @@ local arguments
arguments=( arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--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-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec=[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-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]' '--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]' '--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]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--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-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]' {-e,--select-tcpip}'[Use TCP/IP device]'
@ -45,7 +46,6 @@ arguments=(
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'
@ -63,6 +63,7 @@ arguments=(
'--no-mipmaps[Disable the generation of mipmaps]' '--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]' '--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]' '--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[Disable video forwarding]'
'--no-video-playback[Disable video playback]' '--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
@ -81,6 +82,7 @@ arguments=(
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--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,--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]' {-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)' '--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]' '--start-app=[Start an Android app]'
{-t,--show-touches}'[Show physical touches]' {-t,--show-touches}'[Show physical touches]'
@ -92,6 +94,7 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-v,--version}'[Print the version of scrcpy]' {-v,--version}'[Print the version of scrcpy]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' {-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=[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-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]' '--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" cd "$DEPS_DIR"
. common . common
VERSION=35.0.0 VERSION=35.0.2
FILENAME=platform-tools_r$VERSION-windows.zip FILENAME=platform-tools_r$VERSION-win.zip
PROJECT_DIR=platform-tools-$VERSION PROJECT_DIR=platform-tools-$VERSION-windows
SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -27,6 +27,6 @@ else
rmdir "$ZIP_PREFIX" rmdir "$ZIP_PREFIX"
fi fi
mkdir -p "$INSTALL_DIR/$HOST/bin" mkdir -p "$INSTALL_DIR/adb-windows"
cd "$INSTALL_DIR/$HOST/bin" cd "$INSTALL_DIR/adb-windows"
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/"

View File

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

View File

@ -3,11 +3,12 @@ set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR" cd "$DEPS_DIR"
. common . common
process_args "$@"
VERSION=7.0.2 VERSION=7.1
FILENAME=ffmpeg-$VERSION.tar.xz FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
cd "$SOURCES_DIR" cd "$SOURCES_DIR"
@ -22,68 +23,121 @@ fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR" mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR"
if [[ "$HOST" = win32 ]] if [[ -d "$DIRNAME" ]]
then then
ARCH=x86 echo "'$PWD/$DIRNAME' already exists, not reconfigured"
elif [[ "$HOST" = win64 ]] cd "$DIRNAME"
then
ARCH=x86_64
else else
echo "Unsupported host: $HOST" >&2 mkdir "$DIRNAME"
exit 1 cd "$DIRNAME"
fi
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll if [[ "$HOST" == win* ]]
# -static to avoid dynamic dependency to zlib then
export CFLAGS='-static-libgcc -static' # -static-libgcc to avoid missing libgcc_s_dw2-1.dll
export CXXFLAGS="$CFLAGS" # -static to avoid dynamic dependency to zlib
export LDFLAGS='-static-libgcc -static' export CFLAGS='-static-libgcc -static'
export CXXFLAGS="$CFLAGS"
export LDFLAGS='-static-libgcc -static'
elif [[ "$HOST" == "macos" ]]
then
export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib"
export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include"
if [[ -d "$HOST" ]] export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib"
then export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include"
echo "'$PWD/$HOST' already exists, not reconfigured" export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
cd "$HOST" fi
else
mkdir "$HOST"
cd "$HOST"
"$SOURCES_DIR/$PROJECT_DIR"/configure \ conf=(
--prefix="$INSTALL_DIR/$HOST" \ --prefix="$INSTALL_DIR/$DIRNAME"
--enable-cross-compile \ --extra-cflags="-O2 -fPIC"
--target-os=mingw32 \ --disable-programs
--arch="$ARCH" \ --disable-doc
--cross-prefix="${HOST_TRIPLET}-" \ --disable-swscale
--cc="${HOST_TRIPLET}-gcc" \ --disable-postproc
--extra-cflags="-O2 -fPIC" \ --disable-avfilter
--enable-shared \ --disable-network
--disable-static \ --disable-everything
--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 \
--disable-vulkan --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 fi
make -j make -j

View File

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

View File

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

@ -109,20 +109,22 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
static = get_option('static')
dependencies = [ dependencies = [
dependency('libavformat', version: '>= 57.33'), dependency('libavformat', version: '>= 57.33', static: static),
dependency('libavcodec', version: '>= 57.37'), dependency('libavcodec', version: '>= 57.37', static: static),
dependency('libavutil'), dependency('libavutil', static: static),
dependency('libswresample'), dependency('libswresample', static: static),
dependency('sdl2', version: '>= 2.0.5'), dependency('sdl2', version: '>= 2.0.5', static: static),
] ]
if v4l2_support if v4l2_support
dependencies += dependency('libavdevice') dependencies += dependency('libavdevice', static: static)
endif endif
if usb_support if usb_support
dependencies += dependency('libusb-1.0') dependencies += dependency('libusb-1.0', static: static)
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
@ -167,9 +169,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# run a server debugger and wait for a client to be attached # run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger')) 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) # enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support) conf.set('HAVE_V4L2', v4l2_support)

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "2.7" VALUE "ProductVersion", "3.0"
END END
END END
BLOCK "VarFileInfo" 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 .B \-\-always\-on\-top
Make scrcpy window always on top (above other windows). 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 .TP
.BI "\-\-audio\-bit\-rate " value .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). 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"). 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 .TP
.BI "\-\-camera\-facing " facing .BI "\-\-camera\-facing " facing
Select the device camera by its facing direction. 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. 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 .TP
.BI "\-\-camera\-size " width\fRx\fIheight .BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size. 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 .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. 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 The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).
.B \-\-max\-size
value is computed on the cropped size.
.TP .TP
.B \-d, \-\-select\-usb .B \-d, \-\-select\-usb
@ -139,12 +153,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.BI "\-\-disable\-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. 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 .TP
.BI "\-\-display\-id " id .BI "\-\-display\-id " id
Specify the device display id to mirror. Specify the device display id to mirror.
@ -247,16 +255,6 @@ List video and audio encoders available on the device.
.B \-\-list\-displays .B \-\-list\-displays
List displays available on the device. 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 .TP
.BI "\-m, \-\-max\-size " value .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. 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.
@ -320,14 +318,13 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video
.TP .TP
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] \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, and \fB\-\-max\-size\fR is considered. Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI.
Examples: Examples:
\-\-new\-display=1920x1080 \-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420 \-\-new\-display=1920x1080/420
\-\-new\-display # main display size and density \-\-new\-display # main display size and density
\-\-new\-display -m1920 # scaled to fit a max size of 1920
\-\-new\-display=/240 # main display size and 240 dpi \-\-new\-display=/240 # main display size and 240 dpi
.TP .TP
@ -372,6 +369,10 @@ Do not forward mouse hover (mouse motion without any clicks) events.
.B \-\-no\-power\-on .B \-\-no\-power\-on
Do not power on the device on start. Do not power on the device on start.
.TP
.B \-\-no\-vd\-system\-decorations
Disable virtual display system decorations flag.
.TP .TP
.B \-\-no\-video .B \-\-no\-video
Disable video forwarding. Disable video forwarding.
@ -554,13 +555,19 @@ Default is "info" for release builds, "debug" for debug builds.
.BI "\-\-v4l2-sink " /dev/videoN .BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device. Output to v4l2loopback device.
It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR).
.TP .TP
.BI "\-\-v4l2-buffer " ms .BI "\-\-v4l2-buffer " ms
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. 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). Default is 0 (no buffering).
@ -669,6 +676,10 @@ Pause or re-pause display
.B MOD+Shift+z .B MOD+Shift+z
Unpause display Unpause display
.TP
.B MOD+Shift+r
Reset video capture/encoding
.TP .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)

View File

@ -739,3 +739,21 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
return sc_adb_parse_device_ip(buf); 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

@ -114,4 +114,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
char * char *
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); 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 #endif

View File

@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
// Enable compensation when the difference exceeds +/- 4ms. // Enable compensation when the difference exceeds +/- 4ms.
// Disable compensation when the difference is lower than +/- 1ms. // Disable compensation when the difference is lower than +/- 1ms.
int threshold = ar->compensation != 0 int threshold = ar->compensation_active
? ar->sample_rate / 1000 /* 1ms */ ? ar->sample_rate / 1000 /* 1ms */
: ar->sample_rate * 4 / 1000; /* 4ms */ : ar->sample_rate * 4 / 1000; /* 4ms */
@ -309,14 +309,12 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ar->target_buffering, avg, can_read, diff); " compensation=%d", ar->target_buffering, avg, can_read, diff);
if (diff != ar->compensation) { int ret = swr_set_compensation(swr_ctx, diff, distance);
int ret = swr_set_compensation(swr_ctx, diff, distance); if (ret < 0) {
if (ret < 0) { LOGW("Resampling compensation failed: %d", ret);
LOGW("Resampling compensation failed: %d", ret); // not fatal
// not fatal } else {
} else { ar->compensation_active = diff != 0;
ar->compensation = diff;
}
} }
} }
@ -392,7 +390,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
atomic_init(&ar->played, false); atomic_init(&ar->played, false);
atomic_init(&ar->received, false); atomic_init(&ar->received, false);
atomic_init(&ar->underflow, 0); atomic_init(&ar->underflow, 0);
ar->compensation = 0; ar->compensation_active = false;
return true; return true;

View File

@ -44,8 +44,8 @@ struct sc_audio_regulator {
// Number of silence samples inserted since the last received packet // Number of silence samples inserted since the last received packet
atomic_uint_least32_t underflow; atomic_uint_least32_t underflow;
// Current applied compensation value (only used by the receiver thread) // Non-zero compensation applied (only used by the receiver thread)
int compensation; bool compensation_active;
// Set to true the first time a sample is received // Set to true the first time a sample is received
atomic_bool received; atomic_bool received;

View File

@ -50,6 +50,7 @@ enum {
OPT_POWER_OFF_ON_CLOSE, OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK, OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER, OPT_DISPLAY_BUFFER,
OPT_VIDEO_BUFFER,
OPT_V4L2_BUFFER, OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST, OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT, OPT_TUNNEL_PORT,
@ -105,6 +106,10 @@ enum {
OPT_NEW_DISPLAY, OPT_NEW_DISPLAY,
OPT_LIST_APPS, OPT_LIST_APPS,
OPT_START_APP, OPT_START_APP,
OPT_SCREEN_OFF_TIMEOUT,
OPT_CAPTURE_ORIENTATION,
OPT_ANGLE,
OPT_NO_VD_SYSTEM_DECORATIONS,
}; };
struct sc_option { struct sc_option {
@ -146,6 +151,13 @@ static const struct sc_option options[] = {
.longopt = "always-on-top", .longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).", .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_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate", .longopt = "audio-bit-rate",
@ -243,14 +255,6 @@ static const struct sc_option options[] = {
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. " "ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")." "\"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_id = OPT_CAMERA_FACING,
.longopt = "camera-facing", .longopt = "camera-facing",
@ -258,6 +262,14 @@ static const struct sc_option options[] = {
.text = "Select the device camera by its facing direction.\n" .text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".", "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_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed", .longopt = "camera-high-speed",
@ -265,6 +277,14 @@ static const struct sc_option options[] = {
"This mode is restricted to specific resolutions and frame " "This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.", "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_id = OPT_CAMERA_SIZE,
.longopt = "camera-size", .longopt = "camera-size",
@ -272,12 +292,21 @@ static const struct sc_option options[] = {
.text = "Specify an explicit camera capture size.", .text = "Specify an explicit camera capture size.",
}, },
{ {
.longopt_id = OPT_CAMERA_FPS, .longopt_id = OPT_CAPTURE_ORIENTATION,
.longopt = "camera-fps", .longopt = "capture-orientation",
.argdesc = "value", .argdesc = "value",
.text = "Specify the camera capture frame rate.\n" .text = "Set the capture video orientation.\n"
"If not specified, Android's default frame rate (30 fps) is " "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
"used.", "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 // Not really deprecated (--codec has never been released), but without
@ -300,8 +329,7 @@ static const struct sc_option options[] = {
.argdesc = "width:height:x:y", .argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n" .text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation " "The values are expressed in the device natural orientation "
"(typically, portrait for a phone, landscape for a tablet). " "(typically, portrait for a phone, landscape for a tablet).",
"Any --max-size value is computed on the cropped size.",
}, },
{ {
.shortopt = 'd', .shortopt = 'd',
@ -321,12 +349,10 @@ static const struct sc_option options[] = {
.argdesc = "id", .argdesc = "id",
}, },
{ {
// deprecated
.longopt_id = OPT_DISPLAY_BUFFER, .longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer", .longopt = "display-buffer",
.argdesc = "ms", .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, .longopt_id = OPT_DISPLAY_ID,
@ -471,18 +497,10 @@ static const struct sc_option options[] = {
.text = "List video and audio encoders available on the device.", .text = "List video and audio encoders available on the device.",
}, },
{ {
// deprecated
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation", .longopt = "lock-video-orientation",
.argdesc = "value", .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', .shortopt = 'm',
@ -572,12 +590,11 @@ static const struct sc_option options[] = {
.optional_arg = true, .optional_arg = true,
.text = "Create a new display with the specified resolution and " .text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display " "density. If not provided, they default to the main display "
"dimensions and DPI, and --max-size is considered.\n" "dimensions and DPI.\n"
"Examples:\n" "Examples:\n"
" --new-display=1920x1080\n" " --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display # main display size and density\n" " --new-display # main display size and density\n"
" --new-display -m1920 # scaled to fit a max size of 1920\n"
" --new-display=/240 # main display size and 240 dpi", " --new-display=/240 # main display size and 240 dpi",
}, },
{ {
@ -642,6 +659,11 @@ static const struct sc_option options[] = {
.longopt = "no-power-on", .longopt = "no-power-on",
.text = "Do not power on the device on start.", .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_id = OPT_NO_VIDEO,
.longopt = "no-video", .longopt = "no-video",
@ -794,6 +816,13 @@ static const struct sc_option options[] = {
.longopt = "turn-screen-off", .longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.", .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_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod", .longopt = "shortcut-mod",
@ -888,8 +917,6 @@ static const struct sc_option options[] = {
.longopt = "v4l2-sink", .longopt = "v4l2-sink",
.argdesc = "/dev/videoN", .argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n" .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.", "This feature is only available on Linux.",
}, },
{ {
@ -898,11 +925,20 @@ static const struct sc_option options[] = {
.argdesc = "ms", .argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing " .text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n" "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" "V4L2 sink.\n"
"Default is 0 (no buffering).\n" "Default is 0 (no buffering).\n"
"This option is only available on Linux.", "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_id = OPT_VIDEO_CODEC,
.longopt = "video-codec", .longopt = "video-codec",
@ -1014,6 +1050,10 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+z" }, .shortcuts = { "MOD+Shift+z" },
.text = "Unpause display", .text = "Unpause display",
}, },
{
.shortcuts = { "MOD+Shift+r" },
.text = "Reset video capture/encoding",
},
{ {
.shortcuts = { "MOD+g" }, .shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)", .text = "Resize window to 1:1 (pixel-perfect)",
@ -1562,78 +1602,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) {
return true; 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 static bool
parse_orientation(const char *s, enum sc_orientation *orientation) { parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) { if (!strcmp(s, "0")) {
@ -1673,6 +1641,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) {
return false; 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 static bool
parse_window_position(const char *s, int16_t *position) { parse_window_position(const char *s, int16_t *position) {
// special value for "auto" // special value for "auto"
@ -2143,6 +2137,20 @@ parse_time_limit(const char *s, sc_tick *tick) {
return true; 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 static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) { if (!s || !strcmp(s, "true")) {
@ -2268,8 +2276,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->crop = optarg; opts->crop = optarg;
break; break;
case OPT_DISPLAY: case OPT_DISPLAY:
LOGW("--display is deprecated, use --display-id instead."); LOGE("--display has been removed, use --display-id instead.");
// fall through return false;
case OPT_DISPLAY_ID: case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) { if (!parse_display_id(optarg, &opts->display_id)) {
return false; return false;
@ -2333,8 +2341,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--mouse=uhid instead."); "--mouse=uhid instead.");
return false; return false;
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, LOGE("--lock-video-orientation has been removed, use "
&opts->lock_video_orientation)) { "--capture-orientation instead.");
return false;
case OPT_CAPTURE_ORIENTATION:
if (!parse_capture_orientation(optarg,
&opts->capture_orientation,
&opts->capture_orientation_lock)) {
return false; return false;
} }
break; break;
@ -2352,8 +2365,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->control = false; opts->control = false;
break; break;
case OPT_NO_DISPLAY: case OPT_NO_DISPLAY:
LOGW("--no-display is deprecated, use --no-playback instead."); LOGE("--no-display has been removed, use --no-playback "
// fall through "instead.");
return false;
case 'N': case 'N':
opts->video_playback = false; opts->video_playback = false;
opts->audio_playback = false; opts->audio_playback = false;
@ -2439,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; opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break; break;
case OPT_ROTATION: case OPT_ROTATION:
LOGW("--rotation is deprecated, use --display-orientation " LOGE("--rotation has been removed, use --orientation or "
"instead."); "--capture-orientation instead.");
uint8_t rotation; return false;
if (!parse_rotation(optarg, &rotation)) {
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: case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) { if (!parse_orientation(optarg, &opts->display_orientation)) {
return false; return false;
@ -2525,23 +2516,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
break; break;
case OPT_FORWARD_ALL_CLICKS: case OPT_FORWARD_ALL_CLICKS:
LOGW("--forward-all-clicks is deprecated, " LOGE("--forward-all-clicks has been removed, "
"use --mouse-bind=++++ instead."); "use --mouse-bind=++++ instead.");
opts->mouse_bindings = (struct sc_mouse_bindings) { return false;
.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;
case OPT_LEGACY_PASTE: case OPT_LEGACY_PASTE:
opts->legacy_paste = true; opts->legacy_paste = true;
break; break;
@ -2549,7 +2526,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->power_off_on_close = true; opts->power_off_on_close = true;
break; break;
case OPT_DISPLAY_BUFFER: 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; return false;
} }
break; break;
@ -2714,6 +2695,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_START_APP: case OPT_START_APP:
opts->start_app = optarg; opts->start_app = optarg;
break; 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: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -2808,13 +2801,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false; 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. // V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior, // Do not log because downsizing on error is the default behavior,
// not an explicit request from the user. // not an explicit request from the user.
@ -2904,13 +2890,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("--new-display is incompatible with --no-video"); LOGE("--new-display is incompatible with --no-video");
return false; return false;
} }
if (opts->max_size && opts->new_display[0] != '\0'
&& opts->new_display[0] != '/') {
// An explicit size is defined (not "" nor "/<dpi>")
LOGE("Cannot specify both --new-display size and -m/--max-size");
return false;
}
} }
if (otg) { if (otg) {

View File

@ -22,9 +22,6 @@
#define MOTIONEVENT_ACTION_LABEL(value) \ #define MOTIONEVENT_ACTION_LABEL(value) \
ENUM_TO_LABEL(android_motionevent_action_labels, 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[] = { static const char *const android_keyevent_action_labels[] = {
"down", "down",
"up", "up",
@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = {
"btn-release", "btn-release",
}; };
static const char *const screen_power_mode_labels[] = {
"off",
"doze",
"normal",
"doze-suspend",
"suspend",
};
static const char *const copy_key_labels[] = { static const char *const copy_key_labels[] = {
"none", "none",
"copy", "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, size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len; return 10 + len;
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_display_power.on;
return 2; return 2;
case SC_CONTROL_MSG_TYPE_UHID_CREATE: case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write16be(&buf[1], msg->uhid_create.id); sc_write16be(&buf[1], msg->uhid_create.id);
@ -192,6 +181,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
// no additional data // no additional data
return 1; return 1;
default: default:
@ -268,9 +258,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.paste ? "paste" : "nopaste",
msg->set_clipboard.text); msg->set_clipboard.text);
break; break;
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
LOG_CMSG("power mode %s", LOG_CMSG("display power %s",
SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); msg->set_display_power.on ? "on" : "off");
break; break;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
LOG_CMSG("expand notification panel"); LOG_CMSG("expand notification panel");
@ -315,6 +305,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_START_APP: case SC_CONTROL_MSG_TYPE_START_APP:
LOG_CMSG("start app \"%s\"", msg->start_app.name); LOG_CMSG("start app \"%s\"", msg->start_app.name);
break; break;
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
LOG_CMSG("reset video");
break;
default: default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type); LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break; break;

View File

@ -35,19 +35,14 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_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_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP, SC_CONTROL_MSG_TYPE_START_APP,
}; SC_CONTROL_MSG_TYPE_RESET_VIDEO,
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,
}; };
enum sc_copy_key { enum sc_copy_key {
@ -95,8 +90,8 @@ struct sc_control_msg {
bool paste; bool paste;
} set_clipboard; } set_clipboard;
struct { struct {
enum sc_screen_power_mode mode; bool on;
} set_screen_power_mode; } set_display_power;
struct { struct {
uint16_t id; uint16_t id;
const char *name; // pointer to static data const char *name; // pointer to static data

View File

@ -203,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
} }
static void static void
set_screen_power_mode(struct sc_input_manager *im, set_display_power(struct sc_input_manager *im, bool on) {
enum sc_screen_power_mode mode) {
assert(im->controller); assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
msg.set_screen_power_mode.mode = mode; msg.set_display_power.on = on;
if (!sc_controller_push_msg(im->controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set screen power mode'");
@ -285,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 static void
apply_orientation_transform(struct sc_input_manager *im, apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) { enum sc_orientation transform) {
@ -415,10 +426,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return; return;
case SDLK_o: case SDLK_o:
if (control && !repeat && down && !paused) { if (control && !repeat && down && !paused) {
enum sc_screen_power_mode mode = shift bool on = shift;
? SC_SCREEN_POWER_MODE_NORMAL set_display_power(im, on);
: SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(im, mode);
} }
return; return;
case SDLK_z: case SDLK_z:
@ -524,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
} }
return; return;
case SDLK_r: case SDLK_r:
if (control && !shift && !repeat && down && !paused) { if (control && !repeat && down && !paused) {
rotate_device(im); if (shift) {
reset_video(im);
} else {
rotate_device(im);
}
} }
return; return;
case SDLK_k: case SDLK_k:

View File

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

View File

@ -84,6 +84,12 @@ enum sc_orientation { // v v v
SC_ORIENTATION_FLIP_270, // 1 1 1 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 static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) { sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7)); 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 { enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode 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 video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server 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 display_orientation;
enum sc_orientation record_orientation; enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
@ -259,10 +257,11 @@ struct scrcpy_options {
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint32_t display_id; uint32_t display_id;
sc_tick display_buffer; sc_tick video_buffer;
sc_tick audio_buffer; sc_tick audio_buffer;
sc_tick audio_output_buffer; sc_tick audio_output_buffer;
sc_tick time_limit; sc_tick time_limit;
sc_tick screen_off_timeout;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
const char *v4l2_device; const char *v4l2_device;
sc_tick v4l2_buffer; sc_tick v4l2_buffer;
@ -311,6 +310,7 @@ struct scrcpy_options {
bool audio_dup; bool audio_dup;
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
const char *start_app; const char *start_app;
bool vd_system_decorations;
}; };
extern const struct scrcpy_options scrcpy_options_default; 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; return false;
} }
int ret = avio_open(&recorder->ctx->pb, recorder->filename, char *file_url = sc_str_concat("file:", recorder->filename);
AVIO_FLAG_WRITE); 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) { if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename); LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx); avformat_free_context(recorder->ctx);

View File

@ -53,7 +53,7 @@ struct scrcpy {
struct sc_decoder video_decoder; struct sc_decoder video_decoder;
struct sc_decoder audio_decoder; struct sc_decoder audio_decoder;
struct sc_recorder recorder; struct sc_recorder recorder;
struct sc_delay_buffer display_buffer; struct sc_delay_buffer video_buffer;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink; struct sc_v4l2_sink v4l2_sink;
struct sc_delay_buffer v4l2_buffer; struct sc_delay_buffer v4l2_buffer;
@ -428,7 +428,10 @@ scrcpy(struct scrcpy_options *options) {
.video_bit_rate = options->video_bit_rate, .video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate, .audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps, .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, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.new_display = options->new_display, .new_display = options->new_display,
@ -455,6 +458,7 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on, .power_on = options->power_on,
.kill_adb_on_close = options->kill_adb_on_close, .kill_adb_on_close = options->kill_adb_on_close,
.camera_high_speed = options->camera_high_speed, .camera_high_speed = options->camera_high_speed,
.vd_system_decorations = options->vd_system_decorations,
.list = options->list, .list = options->list,
}; };
@ -815,11 +819,11 @@ aoa_complete:
if (options->video_playback) { if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source; struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) { if (options->video_buffer) {
sc_delay_buffer_init(&s->display_buffer, sc_delay_buffer_init(&s->video_buffer,
options->display_buffer, true); options->video_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); sc_frame_source_add_sink(src, &s->video_buffer.frame_sink);
src = &s->display_buffer.frame_source; src = &s->video_buffer.frame_source;
} }
sc_frame_source_add_sink(src, &s->screen.frame_sink); sc_frame_source_add_sink(src, &s->screen.frame_sink);
@ -873,11 +877,11 @@ aoa_complete:
// everything is set up // everything is set up
if (options->control && options->turn_screen_off) { if (options->control && options->turn_screen_off) {
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; msg.set_display_power.on = false;
if (!sc_controller_push_msg(&s->controller, &msg)) { if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set display power'");
} }
} }

View File

@ -201,18 +201,31 @@ execute_server(struct sc_server *server,
cmd[count++] = "app_process"; cmd[count++] = "app_process";
#ifdef SERVER_DEBUGGER #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" # define SERVER_DEBUGGER_PORT "5005"
cmd[count++] = const char *dbg;
# ifdef SERVER_DEBUGGER_METHOD_NEW if (sdk_version < 28) {
/* Android 9 and above */ // Android < 9
"-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address="
"server=y,address=" SERVER_DEBUGGER_PORT;
# else } else if (sdk_version < 30) {
/* Android 8 and below */ // Android >= 9 && Android < 11
"-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,"
# endif "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT;
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 #endif
cmd[count++] = "/"; // unused cmd[count++] = "/"; // unused
cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = "com.genymobile.scrcpy.Server";
cmd[count++] = SCRCPY_VERSION; cmd[count++] = SCRCPY_VERSION;
@ -274,9 +287,21 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->max_fps); VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps);
} }
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { if (params->angle) {
ADD_PARAM("lock_video_orientation=%" PRIi8, VALIDATE_STRING(params->angle);
params->lock_video_orientation); 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) { if (server->tunnel.forward) {
ADD_PARAM("tunnel_forward=true"); ADD_PARAM("tunnel_forward=true");
@ -320,6 +345,11 @@ execute_server(struct sc_server *server,
if (params->stay_awake) { if (params->stay_awake) {
ADD_PARAM("stay_awake=true"); 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) { if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options); VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options);
@ -359,6 +389,9 @@ execute_server(struct sc_server *server,
VALIDATE_STRING(params->new_display); VALIDATE_STRING(params->new_display);
ADD_PARAM("new_display=%s", 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) { if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true"); ADD_PARAM("list_encoders=true");
} }
@ -380,10 +413,14 @@ execute_server(struct sc_server *server,
cmd[count++] = NULL; cmd[count++] = NULL;
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port " LOGI("Server debugger listening%s...",
SERVER_DEBUGGER_PORT "..."); sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : "");
// From the computer, run // For Android < 11, from the computer:
// adb forward tcp:5005 tcp:5005 // - 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... // Then, from Android Studio: Run > Debug > Edit configurations...
// On the left, click on '+', "Remote", with: // On the left, click on '+', "Remote", with:
// Host: localhost // Host: localhost

View File

@ -45,7 +45,10 @@ struct sc_server_params {
uint32_t video_bit_rate; uint32_t video_bit_rate;
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
const char *max_fps; // float to be parsed by the server 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; bool control;
uint32_t display_id; uint32_t display_id;
const char *new_display; const char *new_display;
@ -66,6 +69,7 @@ struct sc_server_params {
bool power_on; bool power_on;
bool kill_adb_on_close; bool kill_adb_on_close;
bool camera_high_speed; bool camera_high_speed;
bool vd_system_decorations;
uint8_t list; uint8_t list;
}; };

View File

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

View File

@ -7,21 +7,37 @@
#include <stdint.h> #include <stdint.h>
#ifdef _WIN32 #ifdef _WIN32
# include <winsock2.h> # include <winsock2.h>
# include <stdatomic.h> typedef SOCKET sc_raw_socket;
# define SC_SOCKET_NONE NULL # define SC_RAW_SOCKET_NONE INVALID_SOCKET
typedef struct sc_socket_windows {
SOCKET socket;
atomic_flag closed;
} *sc_socket;
#else // not _WIN32 #else // not _WIN32
# include <sys/socket.h> # include <sys/socket.h>
# define SC_SOCKET_NONE -1 # define SC_SOCKET_NONE -1
typedef int sc_socket; 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_wrapper {
sc_raw_socket socket;
atomic_flag closed;
} *sc_socket;
#else
# define SC_SOCKET_NONE -1
typedef sc_raw_socket sc_socket;
#endif #endif
#define IPV4_LOCALHOST 0x7F000001 #define IPV4_LOCALHOST 0x7F000001

View File

@ -64,6 +64,26 @@ sc_str_quote(const char *src) {
return quoted; 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 bool
sc_str_parse_integer(const char *s, long *out) { sc_str_parse_integer(const char *s, long *out) {
char *endptr; char *endptr;

View File

@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
char * char *
sc_str_quote(const char *src); 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` * Parse `s` as an integer into `out`
* *

View File

@ -51,7 +51,6 @@ static void test_options(void) {
"--fullscreen", "--fullscreen",
"--max-fps", "30", "--max-fps", "30",
"--max-size", "1024", "--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-playback" is not compatible with "--fulscreen" // "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236", "--port", "1234:1236",
@ -80,7 +79,6 @@ static void test_options(void) {
assert(opts->fullscreen); assert(opts->fullscreen);
assert(!strcmp(opts->max_fps, "30")); assert(!strcmp(opts->max_fps, "30"));
assert(opts->max_size == 1024); assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234); assert(opts->port_range.first == 1234);
assert(opts->port_range.last == 1236); assert(opts->port_range.last == 1236);
assert(!strcmp(opts->push_target, "/sdcard/Movies")); 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))); 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 = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, .type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
.set_screen_power_mode = { .set_display_power = {
.mode = SC_SCREEN_POWER_MODE_NORMAL, .on = true,
}, },
}; };
@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) {
assert(size == 2); assert(size == 2);
const uint8_t expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
0x02, // SC_SCREEN_POWER_MODE_NORMAL 0x01, // true
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) {
assert(!memcmp(buf, expected, sizeof(expected))); 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[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -423,11 +438,12 @@ int main(int argc, char *argv[]) {
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_clipboard_long(); test_serialize_set_clipboard_long();
test_serialize_set_screen_power_mode(); test_serialize_set_display_power();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_uhid_create(); test_serialize_uhid_create();
test_serialize_uhid_input(); test_serialize_uhid_input();
test_serialize_uhid_destroy(); test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard(); test_serialize_open_hard_keyboard();
test_serialize_reset_video();
return 0; return 0;
} }

View File

@ -141,6 +141,16 @@ static void test_quote(void) {
free(out); 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) { static void test_utf8_truncate(void) {
const char *s = "aÉbÔc"; const char *s = "aÉbÔc";
assert(strlen(s) == 7); // É and Ô are 2 bytes-wide 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_before_sep();
test_join_truncated_after_sep(); test_join_truncated_after_sep();
test_quote(); test_quote();
test_concat();
test_utf8_truncate(); test_utf8_truncate();
test_parse_integer(); test_parse_integer();
test_parse_integers(); test_parse_integers();

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: 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), 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 sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies # 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 # server build dependencies
sudo dnf install java-devel sudo dnf install java-devel
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v2.7`][direct-scrcpy-server] - [`scrcpy-server-v3.0`][direct-scrcpy-server]
<sub>SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba`</sub> <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 Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -23,14 +23,20 @@ To control the device without mirroring:
scrcpy --no-video --no-audio scrcpy --no-video --no-audio
``` ```
By default, mouse mode is switched to UHID if video mirroring is disabled (a By default, the mouse is disabled when video playback is turned off.
relative mouse mode is required).
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: To also use a UHID keyboard, set it explicitly:
```bash ```bash
scrcpy --no-video --no-audio --keyboard=uhid scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid
scrcpy --no-video --no-audio -K # short version scrcpy --no-video --no-audio -MK # short version
``` ```
To use AOA instead (over USB only): To use AOA instead (over USB only):

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) 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 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 decodes the video frames, and displays them as soon as possible, without
buffering (unless `--display-buffer=delay` is specified) to minimize latency. buffering (unless `--video-buffer=delay` is specified) to minimize latency. The
The client is not aware of the device rotation (which is handled by the server), client is not aware of the device rotation (which is handled by the server), it
it just knows the dimensions of the video frames it receives. just knows the dimensions of the video frames it receives.
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS 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 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 meson configure x -Dserver_debugger=true
``` ```
If your device runs Android 8 or below, set the `server_debugger_method` to Then recompile, and run scrcpy.
`old` in addition:
```bash For Android < 11, it will start a debugger on port 5005 on the device and wait:
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.
Redirect that port to the computer: Redirect that port to the computer:
```bash ```bash
adb forward tcp:5005 tcp:5005 adb forward tcp:5005 tcp:5005
``` ```
In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on For Android >= 11, first find the listening port:
`+`, _Remote_, and fill the form:
```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` - Host: `localhost`
- Port: `5005` - 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), If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior). `--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 ## Turn screen off
@ -46,6 +61,40 @@ scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version 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 ## Show touches
@ -62,6 +111,16 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device). 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 ## Power off on close

View File

@ -2,6 +2,23 @@
## Install ## 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> <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: 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` - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))
### Latest version
However, the packaged version is not always the latest release. To install the ### From an install script
latest release from `master`, follow this simplified process.
To install the latest release from `master`, follow this simplified process.
First, you need to install the required packages: First, you need to install the required packages:

View File

@ -2,6 +2,23 @@
## Install ## 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]: Scrcpy is available in [Homebrew]:
```bash ```bash
@ -13,7 +30,7 @@ brew install scrcpy
You need `adb`, accessible from your `PATH`. If you don't have it yet: You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash ```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: Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:

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)_ | 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> | Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</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 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¹_ | 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_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_

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 If encoding fails, scrcpy automatically tries again with a lower definition
(unless `--no-downsize-on-error` is enabled). (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 ## Bit rate
@ -93,7 +96,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one: try another one:
```bash ```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 - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). 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 of the video sent from the device to the computer). This affects the
recording. recording.
- `--orientation` is applied on the client side, and affects display and - `--orientation` is applied on the client side, and affects display and
recording. For the display, it can be changed dynamically using recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md). [shortcuts](shortcuts.md).
To lock the mirroring orientation (on the capture side): To capture the video with a specific orientation:
```bash ```bash
scrcpy --lock-video-orientation # initial (current) orientation scrcpy --capture-orientation=0
scrcpy --lock-video-orientation=0 # natural orientation scrcpy --capture-orientation=90 # 90° clockwise
scrcpy --lock-video-orientation=90 # 90° clockwise scrcpy --capture-orientation=180 # 180°
scrcpy --lock-video-orientation=180 # 180° scrcpy --capture-orientation=270 # 270° clockwise
scrcpy --lock-video-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 ```bash
scrcpy --orientation=0 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. 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 ## Crop
The device screen may be cropped to mirror only part of the screen. 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 The values are expressed in the device natural orientation (portrait for a
phone, landscape for a tablet). 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 ## Display
@ -175,6 +216,8 @@ scrcpy --list-displays
A secondary display may only be controlled if the device runs at least Android A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only). 10 (otherwise it is mirrored as read-only).
It is also possible to create a [virtual display](virtual_display.md).
## Buffering ## Buffering
@ -189,15 +232,15 @@ The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. [v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash ```bash
scrcpy --display-buffer=50 # add 50ms buffering for display scrcpy --video-buffer=50 # add 50ms buffering for video playback
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
scrcpy --audio-buffer=200 # set 200ms buffering for audio 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: They can be applied simultaneously:
```bash ```bash
scrcpy --display-buffer=50 --v4l2-buffer=300 scrcpy --video-buffer=50 --v4l2-buffer=300
``` ```

View File

@ -8,7 +8,6 @@ To mirror a new virtual display instead of the device screen:
scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display # use the main display size and density 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 scrcpy --new-display=/240 # use the main display size and 240 dpi
``` ```
@ -24,3 +23,13 @@ For example:
```bash ```bash
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc 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 ## Install
### From the official release
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit)
<sub>SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5`</sub> <sub>SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be`</sub>
- [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit)
<sub>SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06`</sub> <sub>SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [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-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip
and extract it. and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]:
### From a package manager
From [Chocolatey]:
```bash ```bash
choco install scrcpy choco install scrcpy
choco install adb # if you don't have it yet choco install adb # if you don't have it yet
``` ```
or [Scoop]: From [Scoop]:
```bash ```bash
@ -30,7 +35,6 @@ scoop install scrcpy
scoop install adb # if you don't have it yet scoop install adb # if you don't have it yet
``` ```
[Winget]: https://github.com/microsoft/winget-cli
[Chocolatey]: https://chocolatey.org/ [Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh [Scoop]: https://scoop.sh

View File

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

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '2.7', version: '3.0',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', '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('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt 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('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', 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('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported')
option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported')

View File

@ -1,141 +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 test-client test-server \
build-server \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win64 \
zip-win32 zip-win64 \
package 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)
ZIP := zip
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 build-win32 build-win64 package
clean:
$(GRADLE) clean
rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
test-client:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
test-server:
$(GRADLE) -p server check
test: test-client test-server
build-server:
$(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"
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)"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$(WIN32_BUILD_DIR)/dist"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/"
cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/"
cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/"
cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/"
cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/"
cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/"
cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/"
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)"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$(WIN64_BUILD_DIR)/dist"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/"
cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/"
cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/"
cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/"
cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/"
cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/"
cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/"
zip-win32:
mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)"
cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/"
cd "$(ZIP)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)"
zip-win64:
mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)"
cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/"
cd "$(ZIP)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)"
package: zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(ZIP)/$(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)/"

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)}"

36
release/build_linux.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
LINUX_BUILD_DIR="$WORK_DIR/build-linux"
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/scrcpy_bin"
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

36
release/build_macos.sh Executable file
View File

@ -0,0 +1,36 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd .. # root project dir
MACOS_BUILD_DIR="$WORK_DIR/build-macos"
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/scrcpy_bin"
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy"
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/"

13
release/generate_checksums.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -ex
cd "$(dirname ${BASH_SOURCE[0]})"
. build_common
cd "$OUTPUT_DIR"
sha256sum "scrcpy-server-$VERSION" \
"scrcpy-linux-$VERSION.tar.gz" \
"scrcpy-win32-$VERSION.zip" \
"scrcpy-win64-$VERSION.zip" \
"scrcpy-macos-$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 [[ "$2" != zip && "$2" != tar.gz ]]
then
echo "Invalid format (expected zip or tar.gz): $2" >&2
exit 1
fi
BUILD_DIR="$WORK_DIR/build-$1"
ARCHIVE_DIR="$BUILD_DIR/release-archive"
TARGET="scrcpy-$1-$VERSION"
rm -rf "$ARCHIVE_DIR/$TARGET"
mkdir -p "$ARCHIVE_DIR/$TARGET"
cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET/"
cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/"
mkdir -p "$OUTPUT_DIR"
cd "$ARCHIVE_DIR"
rm -f "$OUTPUT_DIR/$TARGET.$FORMAT"
case "$FORMAT" in
zip)
zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET"
;;
tar.gz)
tar cvf "$OUTPUT_DIR/$TARGET.tar.gz" "$TARGET"
;;
*)
echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2
exit 1
esac
rm -rf "$TARGET"
cd -
echo "Generated '$OUTPUT_DIR/$TARGET.$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
./package_server.sh
./package_client.sh win32 zip
./package_client.sh win64 zip
./package_client.sh linux 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

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 35 targetSdkVersion 35
versionCode 20700 versionCode 30000
versionName "2.7" versionName "3.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -12,10 +12,11 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.7 SCRCPY_VERSION_NAME=3.0
PLATFORM=${ANDROID_PLATFORM:-35} PLATFORM=${ANDROID_PLATFORM:-35}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} 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_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -23,7 +24,8 @@ CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen" GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0") SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server 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" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM" echo "Platform: android-$PLATFORM"
@ -49,12 +51,20 @@ cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. 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=( \ SRC=( \
com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \ com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \ com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \ com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/opengl/*.java \
com/genymobile/scrcpy/util/*.java \ com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \ com/genymobile/scrcpy/wrappers/*.java \
@ -68,10 +78,11 @@ done
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java
javac -bootclasspath "$ANDROID_JAR" \ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \ -d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \ -source 1.8 -target 1.8 \
${FAKE_SRC[@]} \
${SRC[@]} ${SRC[@]}
echo "Dexing..." 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

@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.util.SettingsException;
import android.os.BatteryManager;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
@ -16,59 +18,132 @@ import java.io.OutputStream;
*/ */
public final class CleanUp { public final class CleanUp {
private static final int MSG_TYPE_MASK = 0b11; // Dynamic options
private static final int MSG_TYPE_RESTORE_STAY_ON = 0; private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0;
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; private int pendingChanges;
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; private boolean pendingRestoreDisplayPower;
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
private static final int MSG_PARAM_SHIFT = 2; private Thread thread;
private final OutputStream out; private CleanUp(Options options) {
thread = new Thread(() -> runCleanUp(options), "cleanup");
public CleanUp(OutputStream out) { thread.start();
this.out = out;
} }
public static CleanUp configure(int displayId) throws IOException { public static CleanUp start(Options options) {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; 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); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
Process process = builder.start(); Process process = builder.start();
return new CleanUp(process.getOutputStream()); OutputStream out = process.getOutputStream();
}
private boolean sendMessage(int type, int param) { while (true) {
assert (type & ~MSG_TYPE_MASK) == 0; int localPendingChanges;
int msg = type | param << MSG_PARAM_SHIFT; boolean localPendingRestoreDisplayPower;
try { synchronized (this) {
out.write(msg); while (pendingChanges == 0) {
out.flush(); wait();
return true; }
} catch (IOException e) { localPendingChanges = pendingChanges;
Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); localPendingRestoreDisplayPower = pendingRestoreDisplayPower;
return false; pendingChanges = 0;
}
if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) {
out.write(localPendingRestoreDisplayPower ? 1 : 0);
out.flush();
}
} }
} }
public boolean setRestoreStayOn(int restoreValue) { public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) {
// Restore the value (between 0 and 7), -1 to not restore pendingRestoreDisplayPower = restoreDisplayPower;
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN> pendingChanges |= PENDING_CHANGE_DISPLAY_POWER;
assert restoreValue >= -1 && restoreValue <= 7; notify();
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 static void unlinkSelf() { public static void unlinkSelf() {
@ -83,35 +158,21 @@ public final class CleanUp {
unlinkSelf(); unlinkSelf();
int displayId = Integer.parseInt(args[0]); 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; // Dynamic option
boolean disableShowTouches = false; boolean restoreDisplayPower = false;
boolean restoreNormalPowerMode = false;
boolean powerOffScreen = false;
try { try {
// Wait for the server to die // Wait for the server to die
int msg; int msg;
while ((msg = System.in.read()) != -1) { while ((msg = System.in.read()) != -1) {
int type = msg & MSG_TYPE_MASK; // Only restore display power
int param = msg >> MSG_PARAM_SHIFT; assert msg == 0 || msg == 1;
switch (type) { restoreDisplayPower = msg != 0;
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;
}
} }
} catch (IOException e) { } catch (IOException e) {
// Expected when the server is dead // Expected when the server is dead
@ -137,15 +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) { if (powerOffScreen) {
if (displayId != Device.DISPLAY_ID_NONE) { Ln.i("Power off screen");
Ln.i("Power off screen"); Device.powerOffScreen(targetDisplayId);
Device.powerOffScreen(displayId); } else if (restoreDisplayPower) {
} Ln.i("Restoring display power");
} else if (restoreNormalPowerMode) { Device.setDisplayPower(targetDisplayId, true);
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }

View File

@ -1,9 +1,14 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.content.IContentProvider;
import android.os.Binder;
import android.os.Process; import android.os.Process;
public final class FakeContext extends ContextWrapper { public final class FakeContext extends ContextWrapper {
@ -17,6 +22,38 @@ public final class FakeContext extends ContextWrapper {
return INSTANCE; 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() { private FakeContext() {
super(Workarounds.getSystemContext()); super(Workarounds.getSystemContext());
} }
@ -49,4 +86,9 @@ public final class FakeContext extends ContextWrapper {
public Context getApplicationContext() { public Context getApplicationContext() {
return this; return this;
} }
@Override
public ContentResolver getContentResolver() {
return contentResolver;
}
} }

View File

@ -2,7 +2,9 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
@ -12,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Pair;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -31,7 +34,7 @@ public class Options {
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private float maxFps; private float maxFps;
private int lockVideoOrientation = -1; private float angle;
private boolean tunnelForward; private boolean tunnelForward;
private Rect crop; private Rect crop;
private boolean control = true; private boolean control = true;
@ -44,6 +47,7 @@ public class Options {
private boolean cameraHighSpeed; private boolean cameraHighSpeed;
private boolean showTouches; private boolean showTouches;
private boolean stayAwake; private boolean stayAwake;
private int screenOffTimeout = -1;
private List<CodecOption> videoCodecOptions; private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions; private List<CodecOption> audioCodecOptions;
@ -56,6 +60,10 @@ public class Options {
private boolean powerOn = true; private boolean powerOn = true;
private NewDisplay newDisplay; private NewDisplay newDisplay;
private boolean vdSystemDecorations = true;
private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked;
private Orientation captureOrientation = Orientation.Orient0;
private boolean listEncoders; private boolean listEncoders;
private boolean listDisplays; private boolean listDisplays;
@ -121,8 +129,8 @@ public class Options {
return maxFps; return maxFps;
} }
public int getLockVideoOrientation() { public float getAngle() {
return lockVideoOrientation; return angle;
} }
public boolean isTunnelForward() { public boolean isTunnelForward() {
@ -173,6 +181,10 @@ public class Options {
return stayAwake; return stayAwake;
} }
public int getScreenOffTimeout() {
return screenOffTimeout;
}
public List<CodecOption> getVideoCodecOptions() { public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions; return videoCodecOptions;
} }
@ -213,6 +225,18 @@ public class Options {
return newDisplay; return newDisplay;
} }
public Orientation getCaptureOrientation() {
return captureOrientation;
}
public Orientation.Lock getCaptureOrientationLock() {
return captureOrientationLock;
}
public boolean getVDSystemDecorations() {
return vdSystemDecorations;
}
public boolean getList() { public boolean getList() {
return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; return listEncoders || listDisplays || listCameras || listCameraSizes || listApps;
} }
@ -335,8 +359,8 @@ public class Options {
case "max_fps": case "max_fps":
options.maxFps = parseFloat("max_fps", value); options.maxFps = parseFloat("max_fps", value);
break; break;
case "lock_video_orientation": case "angle":
options.lockVideoOrientation = Integer.parseInt(value); options.angle = parseFloat("angle", value);
break; break;
case "tunnel_forward": case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value); options.tunnelForward = Boolean.parseBoolean(value);
@ -358,6 +382,12 @@ public class Options {
case "stay_awake": case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value); options.stayAwake = Boolean.parseBoolean(value);
break; 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": case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value); options.videoCodecOptions = CodecOption.parse(value);
break; break;
@ -436,6 +466,14 @@ public class Options {
case "new_display": case "new_display":
options.newDisplay = parseNewDisplay(value); options.newDisplay = parseNewDisplay(value);
break; 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": case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value); options.sendDeviceMeta = Boolean.parseBoolean(value);
break; break;
@ -463,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; return options;
} }
@ -554,4 +597,25 @@ public class Options {
return new NewDisplay(size, dpi); 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

@ -14,10 +14,9 @@ import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; 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.CameraCapture;
import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.NewDisplayCapture;
import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.ScreenCapture;
@ -25,7 +24,6 @@ import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.File; import java.io.File;
@ -76,51 +74,6 @@ public final class Server {
// not instantiable // 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 { private static void scrcpy(Options options) throws IOException, ConfigurationException {
if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && 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"); Ln.e("Camera mirroring is not supported before Android 12");
@ -133,14 +86,9 @@ public final class Server {
} }
CleanUp cleanUp = null; CleanUp cleanUp = null;
Thread initThread = null;
NewDisplay newDisplay = options.getNewDisplay();
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
if (options.getCleanup()) { if (options.getCleanup()) {
cleanUp = CleanUp.configure(displayId); cleanUp = CleanUp.start(options);
initThread = startInitThread(options, cleanUp);
} }
int scid = options.getScid(); int scid = options.getScid();
@ -164,7 +112,7 @@ public final class Server {
if (control) { if (control) {
ControlChannel controlChannel = connection.getControlChannel(); ControlChannel controlChannel = connection.getControlChannel();
controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); controller = new Controller(controlChannel, cleanUp, options);
asyncProcessors.add(controller); asyncProcessors.add(controller);
} }
@ -183,8 +131,7 @@ public final class Server {
if (audioCodec == AudioCodec.RAW) { if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else { } else {
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options);
options.getAudioEncoder());
} }
asyncProcessors.add(audioRecorder); asyncProcessors.add(audioRecorder);
} }
@ -194,20 +141,22 @@ public final class Server {
options.getSendFrameMeta()); options.getSendFrameMeta());
SurfaceCapture surfaceCapture; SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) { if (options.getVideoSource() == VideoSource.DISPLAY) {
NewDisplay newDisplay = options.getNewDisplay();
if (newDisplay != null) { if (newDisplay != null) {
surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); surfaceCapture = new NewDisplayCapture(controller, options);
} else { } else {
assert displayId != Device.DISPLAY_ID_NONE; assert options.getDisplayId() != Device.DISPLAY_ID_NONE;
surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(), surfaceCapture = new ScreenCapture(controller, options);
options.getLockVideoOrientation());
} }
} else { } else {
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), surfaceCapture = new CameraCapture(options);
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
} }
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options);
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(surfaceEncoder); asyncProcessors.add(surfaceEncoder);
if (controller != null) {
controller.setSurfaceCapture(surfaceCapture);
}
} }
Completion completion = new Completion(asyncProcessors.size()); Completion completion = new Completion(asyncProcessors.size());
@ -219,22 +168,25 @@ public final class Server {
completion.await(); completion.await();
} finally { } finally {
if (initThread != null) { if (cleanUp != null) {
initThread.interrupt(); cleanUp.interrupt();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
OpenGLRunner.quit(); // quit the OpenGL thread, if any
connection.shutdown(); connection.shutdown();
try { try {
if (initThread != null) { if (cleanUp != null) {
initThread.join(); cleanUp.join();
} }
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.join(); asyncProcessor.join();
} }
OpenGLRunner.join();
} catch (InterruptedException e) { } catch (InterruptedException e) {
// ignore // ignore
} }
@ -243,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) { public static void main(String... args) {
int status = 0; int status = 0;
try { try {

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.Codec;
@ -67,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended; 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.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.bitRate = bitRate; this.bitRate = options.getAudioBitRate();
this.codecOptions = codecOptions; this.codecOptions = options.getAudioCodecOptions();
this.encoderName = encoderName; this.encoderName = options.getAudioEncoder();
} }
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) { private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {

View File

@ -17,13 +17,14 @@ public final class ControlMessage {
public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_SET_DISPLAY_POWER = 10;
public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_ROTATE_DEVICE = 11;
public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_CREATE = 12;
public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_INPUT = 13;
public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_UHID_DESTROY = 14;
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16; public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final long SEQUENCE_INVALID = 0; public static final long SEQUENCE_INVALID = 0;
@ -34,7 +35,7 @@ public final class ControlMessage {
private int type; private int type;
private String text; private String text;
private int metaState; // KeyEvent.META_* private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_*
private int keycode; // KeyEvent.KEYCODE_* private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_* private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_*
@ -49,6 +50,7 @@ public final class ControlMessage {
private long sequence; private long sequence;
private int id; private int id;
private byte[] data; private byte[] data;
private boolean on;
private ControlMessage() { private ControlMessage() {
} }
@ -116,13 +118,10 @@ public final class ControlMessage {
return msg; return msg;
} }
/** public static ControlMessage createSetDisplayPower(boolean on) {
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_SCREEN_POWER_MODE; msg.type = TYPE_SET_DISPLAY_POWER;
msg.action = mode; msg.on = on;
return msg; return msg;
} }
@ -234,4 +233,8 @@ public final class ControlMessage {
public byte[] getData() { public byte[] getData() {
return data; return data;
} }
public boolean getOn() {
return on;
}
} }

View File

@ -39,13 +39,14 @@ public class ControlMessageReader {
return parseGetClipboard(); return parseGetClipboard();
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
return parseSetClipboard(); return parseSetClipboard();
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_DISPLAY_POWER:
return parseSetScreenPowerMode(); return parseSetDisplayPower();
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
case ControlMessage.TYPE_RESET_VIDEO:
return ControlMessage.createEmpty(type); return ControlMessage.createEmpty(type);
case ControlMessage.TYPE_UHID_CREATE: case ControlMessage.TYPE_UHID_CREATE:
return parseUhidCreate(); return parseUhidCreate();
@ -134,9 +135,9 @@ public class ControlMessageReader {
return ControlMessage.createSetClipboard(sequence, text, paste); return ControlMessage.createSetClipboard(sequence, text, paste);
} }
private ControlMessage parseSetScreenPowerMode() throws IOException { private ControlMessage parseSetDisplayPower() throws IOException {
int mode = dis.readUnsignedByte(); boolean on = dis.readBoolean();
return ControlMessage.createSetScreenPowerMode(mode); return ControlMessage.createSetDisplayPower(on);
} }
private ControlMessage parseUhidCreate() throws IOException { private ControlMessage parseUhidCreate() throws IOException {

View File

@ -3,12 +3,15 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DeviceApp;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.video.VirtualDisplayListener;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
@ -91,14 +94,17 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepPowerModeOff; private boolean keepDisplayPowerOff;
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { // Used for resetting video encoding on RESET_VIDEO message
this.displayId = displayId; private SurfaceCapture surfaceCapture;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.displayId = options.getDisplayId();
this.controlChannel = controlChannel; this.controlChannel = controlChannel;
this.cleanUp = cleanUp; this.cleanUp = cleanUp;
this.clipboardAutosync = clipboardAutosync; this.clipboardAutosync = options.getClipboardAutosync();
this.powerOn = powerOn; this.powerOn = options.getPowerOn();
initPointers(); initPointers();
sender = new DeviceMessageSender(controlChannel); sender = new DeviceMessageSender(controlChannel);
@ -143,6 +149,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
} }
public void setSurfaceCapture(SurfaceCapture surfaceCapture) {
this.surfaceCapture = surfaceCapture;
}
private UhidManager getUhidManager() { private UhidManager getUhidManager() {
if (uhidManager == null) { if (uhidManager == null) {
uhidManager = new UhidManager(sender); uhidManager = new UhidManager(sender);
@ -166,7 +176,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException { private void control() throws IOException {
// on start, power on the device // on start, power on the device
if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) { if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack // dirty hack
@ -270,18 +280,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_DISPLAY_POWER:
if (supportsInputEvents) { if (supportsInputEvents) {
int mode = msg.getAction(); setDisplayPower(msg.getOn());
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
if (cleanUp != null) {
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
}
}
} }
break; break;
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
@ -302,6 +303,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
case ControlMessage.TYPE_START_APP: case ControlMessage.TYPE_START_APP:
startAppAsync(msg.getText()); startAppAsync(msg.getText());
break; break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
break;
default: default:
// do nothing // do nothing
} }
@ -310,8 +314,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
schedulePowerModeOff(); assert displayId != Device.DISPLAY_ID_NONE;
scheduleDisplayPowerOff(displayId);
} }
return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
} }
@ -355,7 +360,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Point point = displayData.positionMapper.map(position); Point point = displayData.positionMapper.map(position);
if (point == null) { if (point == null) {
Ln.w("Ignore touch event, it was generated for a different device size"); if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Size eventSize = position.getScreenSize();
Size currentSize = displayData.positionMapper.getVideoSize();
Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")");
}
return false; return false;
} }
@ -469,7 +478,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
Point point = displayData.positionMapper.map(position); Point point = displayData.positionMapper.map(position);
if (point == null) { if (point == null) {
Ln.w("Ignore scroll event, it was generated for a different device size"); if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Size eventSize = position.getScreenSize();
Size currentSize = displayData.positionMapper.getVideoSize();
Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")");
}
return false; return false;
} }
@ -488,17 +501,17 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
} }
/** /**
* Schedule a call to set power mode to off after a small delay. * Schedule a call to set display power to off after a small delay.
*/ */
private static void schedulePowerModeOff() { private static void scheduleDisplayPowerOff(int displayId) {
EXECUTOR.schedule(() -> { EXECUTOR.schedule(() -> {
Ln.i("Forcing screen off"); Ln.i("Forcing display off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF); Device.setDisplayPower(displayId, false);
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }
private boolean pressBackOrTurnScreenOn(int action) { private boolean pressBackOrTurnScreenOn(int action) {
if (Device.isScreenOn()) { if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) {
return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
} }
@ -509,8 +522,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return true; return true;
} }
if (keepPowerModeOff) { if (keepDisplayPowerOff) {
schedulePowerModeOff(); assert displayId != Device.DISPLAY_ID_NONE;
scheduleDisplayPowerOff(displayId);
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
} }
@ -675,4 +689,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return data; return data;
} }
} }
private void setDisplayPower(boolean on) {
// Change the power of the main display when mirroring a virtual display
int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0;
boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on);
if (setDisplayPowerOk) {
// Do not keep display power off for virtual displays: MOD+p must wake up the physical device
keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on;
Ln.i("Device display turned " + (on ? "on" : "off"));
if (cleanUp != null) {
boolean mustRestoreOnExit = !on;
cleanUp.setRestoreDisplayPower(mustRestoreOnExit);
}
}
}
private void resetVideo() {
if (surfaceCapture != null) {
Ln.i("Video capture reset");
surfaceCapture.requestInvalidate();
}
}
} }

View File

@ -3,46 +3,46 @@ package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.util.AffineMatrix;
import android.graphics.Rect;
public final class PositionMapper { public final class PositionMapper {
private final Size videoSize; private final Size videoSize;
private final Rect contentRect; private final AffineMatrix videoToDeviceMatrix;
private final int coordsRotation;
public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) { public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) {
this.videoSize = videoSize; this.videoSize = videoSize;
this.contentRect = contentRect; this.videoToDeviceMatrix = videoToDeviceMatrix;
this.coordsRotation = reverseRotation(videoRotation);
} }
public static PositionMapper from(ScreenInfo screenInfo) { public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) {
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null;
Size videoSize = screenInfo.getUnlockedVideoSize(); AffineMatrix transform = filterTransform;
return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation()); if (convertToPixels) {
AffineMatrix inputTransform = AffineMatrix.ndcFromPixels(videoSize);
AffineMatrix outputTransform = AffineMatrix.ndcToPixels(targetSize);
transform = outputTransform.multiply(transform).multiply(inputTransform);
}
return new PositionMapper(videoSize, transform);
} }
private static int reverseRotation(int rotation) { public Size getVideoSize() {
return (4 - rotation) % 4; return videoSize;
} }
public Point map(Position position) { public Point map(Position position) {
// reverse the video rotation to apply the events Size clientVideoSize = position.getScreenSize();
Position devicePosition = position.rotate(coordsRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!videoSize.equals(clientVideoSize)) { if (!videoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions, // The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event // the device may have been rotated since the event was generated, so ignore the event
return null; return null;
} }
Point point = devicePosition.getPoint(); Point point = position.getPoint();
int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); if (videoToDeviceMatrix != null) {
int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); point = videoToDeviceMatrix.apply(point);
return new Point(convertedX, convertedY); }
return point;
} }
} }

View File

@ -40,8 +40,9 @@ public final class Device {
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; // The new display power method introduced in Android 15 does not work as expected:
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; // <https://github.com/Genymobile/scrcpy/issues/5530>
private static final boolean USE_ANDROID_15_DISPLAY_POWER = false;
private Device() { private Device() {
// not instantiable // not instantiable
@ -80,8 +81,9 @@ public final class Device {
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
} }
public static boolean isScreenOn() { public static boolean isScreenOn(int displayId) {
return ServiceManager.getPowerManager().isScreenOn(); assert displayId != DISPLAY_ID_NONE;
return ServiceManager.getPowerManager().isScreenOn(displayId);
} }
public static void expandNotificationPanel() { public static void expandNotificationPanel() {
@ -126,10 +128,13 @@ public final class Device {
return clipboardManager.setText(text); return clipboardManager.setText(text);
} }
/** public static boolean setDisplayPower(int displayId, boolean on) {
* @param mode one of the {@code POWER_MODE_*} constants assert displayId != Device.DISPLAY_ID_NONE;
*/
public static boolean setScreenPowerMode(int mode) { if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on);
}
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
if (applyToMultiPhysicalDisplays if (applyToMultiPhysicalDisplays
@ -142,6 +147,7 @@ public final class Device {
applyToMultiPhysicalDisplays = false; applyToMultiPhysicalDisplays = false;
} }
int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF;
if (applyToMultiPhysicalDisplays) { if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl // On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl = boolean useDisplayControl =
@ -175,7 +181,7 @@ public final class Device {
public static boolean powerOffScreen(int displayId) { public static boolean powerOffScreen(int displayId) {
assert displayId != DISPLAY_ID_NONE; assert displayId != DISPLAY_ID_NONE;
if (!isScreenOn()) { if (!isScreenOn(displayId)) {
return true; return true;
} }
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);

View File

@ -0,0 +1,47 @@
package com.genymobile.scrcpy.device;
public enum Orientation {
// @formatter:off
Orient0("0"),
Orient90("90"),
Orient180("180"),
Orient270("270"),
Flip0("flip0"),
Flip90("flip90"),
Flip180("flip180"),
Flip270("flip270");
public enum Lock {
Unlocked, LockedInitial, LockedValue,
}
private final String name;
Orientation(String name) {
this.name = name;
}
public static Orientation getByName(String name) {
for (Orientation orientation : values()) {
if (orientation.name.equals(name)) {
return orientation;
}
}
throw new IllegalArgumentException("Unknown orientation: " + name);
}
public static Orientation fromRotation(int rotation) {
assert rotation >= 0 && rotation < 4;
return values()[rotation];
}
public boolean isFlipped() {
return (ordinal() & 4) != 0;
}
public int getRotation() {
return ordinal() & 3;
}
}

View File

@ -29,6 +29,61 @@ public final class Size {
return new Size(height, width); return new Size(height, width);
} }
public Size limit(int maxSize) {
assert maxSize >= 0 : "Max size may not be negative";
assert maxSize % 8 == 0 : "Max size must be a multiple of 8";
if (maxSize == 0) {
// No limit
return this;
}
boolean portrait = height > width;
int major = portrait ? height : width;
if (major <= maxSize) {
return this;
}
int minor = portrait ? width : height;
int newMajor = maxSize;
int newMinor = maxSize * minor / major;
int w = portrait ? newMinor : newMajor;
int h = portrait ? newMajor : newMinor;
return new Size(w, h);
}
/**
* Round both dimensions of this size to be a multiple of 8 (as required by many encoders).
*
* @return The current size rounded.
*/
public Size round8() {
if (isMultipleOf8()) {
// Already a multiple of 8
return this;
}
boolean portrait = height > width;
int major = portrait ? height : width;
int minor = portrait ? width : height;
major &= ~7; // round down to not exceed the initial size
minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion
if (minor > major) {
minor = major;
}
int w = portrait ? minor : major;
int h = portrait ? major : minor;
return new Size(w, h);
}
public boolean isMultipleOf8() {
return (width & 7) == 0 && (height & 7) == 0;
}
public Rect toRect() { public Rect toRect() {
return new Rect(0, 0, width, height); return new Rect(0, 0, width, height);
} }
@ -52,6 +107,6 @@ public final class Size {
@Override @Override
public String toString() { public String toString() {
return "Size{" + "width=" + width + ", height=" + height + '}'; return width + "x" + height;
} }
} }

View File

@ -0,0 +1,135 @@
package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.util.AffineMatrix;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import java.nio.FloatBuffer;
public class AffineOpenGLFilter implements OpenGLFilter {
private int program;
private FloatBuffer vertexBuffer;
private FloatBuffer texCoordsBuffer;
private final float[] userMatrix;
private int vertexPosLoc;
private int texCoordsInLoc;
private int texLoc;
private int texMatrixLoc;
private int userMatrixLoc;
public AffineOpenGLFilter(AffineMatrix transform) {
userMatrix = transform.to4x4();
}
@Override
public void init() throws OpenGLException {
// @formatter:off
String vertexShaderCode = "#version 100\n"
+ "attribute vec4 vertex_pos;\n"
+ "attribute vec4 tex_coords_in;\n"
+ "varying vec2 tex_coords;\n"
+ "uniform mat4 tex_matrix;\n"
+ "uniform mat4 user_matrix;\n"
+ "void main() {\n"
+ " gl_Position = vertex_pos;\n"
+ " tex_coords = (tex_matrix * user_matrix * tex_coords_in).xy;\n"
+ "}";
// @formatter:off
String fragmentShaderCode = "#version 100\n"
+ "#extension GL_OES_EGL_image_external : require\n"
+ "precision highp float;\n"
+ "uniform samplerExternalOES tex;\n"
+ "varying vec2 tex_coords;\n"
+ "void main() {\n"
+ " if (tex_coords.x >= 0.0 && tex_coords.x <= 1.0\n"
+ " && tex_coords.y >= 0.0 && tex_coords.y <= 1.0) {\n"
+ " gl_FragColor = texture2D(tex, tex_coords);\n"
+ " } else {\n"
+ " gl_FragColor = vec4(0.0);\n"
+ " }\n"
+ "}";
program = GLUtils.createProgram(vertexShaderCode, fragmentShaderCode);
if (program == 0) {
throw new OpenGLException("Cannot create OpenGL program");
}
float[] vertices = {
-1, -1, // Bottom-left
1, -1, // Bottom-right
-1, 1, // Top-left
1, 1, // Top-right
};
float[] texCoords = {
0, 0, // Bottom-left
1, 0, // Bottom-right
0, 1, // Top-left
1, 1, // Top-right
};
// OpenGL will fill the 3rd and 4th coordinates of the vec4 automatically with 0.0 and 1.0 respectively
vertexBuffer = GLUtils.createFloatBuffer(vertices);
texCoordsBuffer = GLUtils.createFloatBuffer(texCoords);
vertexPosLoc = GLES20.glGetAttribLocation(program, "vertex_pos");
assert vertexPosLoc != -1;
texCoordsInLoc = GLES20.glGetAttribLocation(program, "tex_coords_in");
assert texCoordsInLoc != -1;
texLoc = GLES20.glGetUniformLocation(program, "tex");
assert texLoc != -1;
texMatrixLoc = GLES20.glGetUniformLocation(program, "tex_matrix");
assert texMatrixLoc != -1;
userMatrixLoc = GLES20.glGetUniformLocation(program, "user_matrix");
assert userMatrixLoc != -1;
}
@Override
public void draw(int textureId, float[] texMatrix) {
GLES20.glUseProgram(program);
GLUtils.checkGlError();
GLES20.glEnableVertexAttribArray(vertexPosLoc);
GLUtils.checkGlError();
GLES20.glEnableVertexAttribArray(texCoordsInLoc);
GLUtils.checkGlError();
GLES20.glVertexAttribPointer(vertexPosLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLUtils.checkGlError();
GLES20.glVertexAttribPointer(texCoordsInLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordsBuffer);
GLUtils.checkGlError();
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLUtils.checkGlError();
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLUtils.checkGlError();
GLES20.glUniform1i(texLoc, 0);
GLUtils.checkGlError();
GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, texMatrix, 0);
GLUtils.checkGlError();
GLES20.glUniformMatrix4fv(userMatrixLoc, 1, false, userMatrix, 0);
GLUtils.checkGlError();
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLUtils.checkGlError();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLUtils.checkGlError();
}
@Override
public void release() {
GLES20.glDeleteProgram(program);
GLUtils.checkGlError();
}
}

View File

@ -0,0 +1,124 @@
package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.BuildConfig;
import com.genymobile.scrcpy.util.Ln;
import android.opengl.GLES20;
import android.opengl.GLU;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
public final class GLUtils {
private static final boolean DEBUG = BuildConfig.DEBUG;
private GLUtils() {
// not instantiable
}
public static int createProgram(String vertexSource, String fragmentSource) {
int vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
int fragmentShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (fragmentShader == 0) {
GLES20.glDeleteShader(vertexShader);
return 0;
}
int program = GLES20.glCreateProgram();
if (program == 0) {
GLES20.glDeleteShader(fragmentShader);
GLES20.glDeleteShader(vertexShader);
return 0;
}
GLES20.glAttachShader(program, vertexShader);
checkGlError();
GLES20.glAttachShader(program, fragmentShader);
checkGlError();
GLES20.glLinkProgram(program);
checkGlError();
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] == 0) {
Ln.e("Could not link program: " + GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
GLES20.glDeleteShader(fragmentShader);
GLES20.glDeleteShader(vertexShader);
return 0;
}
return program;
}
public static int createShader(int type, String source) {
int shader = GLES20.glCreateShader(type);
if (shader == 0) {
Ln.e(getGlErrorMessage("Could not create shader"));
return 0;
}
GLES20.glShaderSource(shader, source);
GLES20.glCompileShader(shader);
int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
Ln.e("Could not compile " + getShaderTypeString(type) + ": " + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
return 0;
}
return shader;
}
private static String getShaderTypeString(int type) {
switch (type) {
case GLES20.GL_VERTEX_SHADER:
return "vertex shader";
case GLES20.GL_FRAGMENT_SHADER:
return "fragment shader";
default:
return "shader";
}
}
/**
* Throws a runtime exception if {@link GLES20#glGetError()} returns an error (useful for debugging).
*/
public static void checkGlError() {
if (DEBUG) {
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
throw new RuntimeException(toErrorString(error));
}
}
}
public static String getGlErrorMessage(String userError) {
int glError = GLES20.glGetError();
if (glError == GLES20.GL_NO_ERROR) {
return userError;
}
return userError + " (" + toErrorString(glError) + ")";
}
private static String toErrorString(int glError) {
String errorString = GLU.gluErrorString(glError);
return "glError 0x" + Integer.toHexString(glError) + " " + errorString;
}
public static FloatBuffer createFloatBuffer(float[] values) {
FloatBuffer fb = ByteBuffer.allocateDirect(values.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
fb.put(values);
fb.position(0);
return fb;
}
}

View File

@ -0,0 +1,13 @@
package com.genymobile.scrcpy.opengl;
import java.io.IOException;
public class OpenGLException extends IOException {
public OpenGLException(String message) {
super(message);
}
public OpenGLException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,21 @@
package com.genymobile.scrcpy.opengl;
public interface OpenGLFilter {
/**
* Initialize the OpenGL filter (typically compile the shaders and create the program).
*
* @throws OpenGLException if an initialization error occurs
*/
void init() throws OpenGLException;
/**
* Render a frame (call for each frame).
*/
void draw(int textureId, float[] texMatrix);
/**
* Release resources.
*/
void release();
}

View File

@ -0,0 +1,258 @@
package com.genymobile.scrcpy.opengl;
import com.genymobile.scrcpy.device.Size;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.Surface;
import java.util.concurrent.Semaphore;
public final class OpenGLRunner {
private static HandlerThread handlerThread;
private static Handler handler;
private static boolean quit;
private EGLDisplay eglDisplay;
private EGLContext eglContext;
private EGLSurface eglSurface;
private final OpenGLFilter filter;
private final float[] overrideTransformMatrix;
private SurfaceTexture surfaceTexture;
private Surface inputSurface;
private int textureId;
private boolean stopped;
public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) {
this.filter = filter;
this.overrideTransformMatrix = overrideTransformMatrix;
}
public OpenGLRunner(OpenGLFilter filter) {
this(filter, null);
}
public static synchronized void initOnce() {
if (handlerThread == null) {
if (quit) {
throw new IllegalStateException("Could not init OpenGLRunner after it is quit");
}
handlerThread = new HandlerThread("OpenGLRunner");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
}
public static void quit() {
HandlerThread thread;
synchronized (OpenGLRunner.class) {
thread = handlerThread;
quit = true;
}
if (thread != null) {
thread.quitSafely();
}
}
public static void join() throws InterruptedException {
HandlerThread thread;
synchronized (OpenGLRunner.class) {
thread = handlerThread;
}
if (thread != null) {
thread.join();
}
}
public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
initOnce();
// Simulate CompletableFuture, but working for all Android versions
final Semaphore sem = new Semaphore(0);
Throwable[] throwableRef = new Throwable[1];
// The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly.
// See <https://github.com/Genymobile/scrcpy/issues/5444>
handler.post(() -> {
try {
run(inputSize, outputSize, outputSurface);
} catch (Throwable throwable) {
throwableRef[0] = throwable;
} finally {
sem.release();
}
});
try {
sem.acquire();
} catch (InterruptedException e) {
// Behave as if this method call was synchronous
Thread.currentThread().interrupt();
}
Throwable throwable = throwableRef[0];
if (throwable != null) {
if (throwable instanceof OpenGLException) {
throw (OpenGLException) throwable;
}
throw new OpenGLException("Asynchronous OpenGL runner init failed", throwable);
}
// Synchronization is ok: inputSurface is written before sem.release() and read after sem.acquire()
return inputSurface;
}
private void run(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException {
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
throw new OpenGLException("Unable to get EGL14 display");
}
int[] version = new int[2];
if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
throw new OpenGLException("Unable to initialize EGL14");
}
// @formatter:off
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0);
if (numConfigs[0] <= 0) {
EGL14.eglTerminate(eglDisplay);
throw new OpenGLException("Unable to find ES2 EGL config");
}
EGLConfig eglConfig = configs[0];
// @formatter:off
int[] contextAttribList = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribList, 0);
if (eglContext == null) {
EGL14.eglTerminate(eglDisplay);
throw new OpenGLException("Failed to create EGL context");
}
int[] surfaceAttribList = {
EGL14.EGL_NONE
};
eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, outputSurface, surfaceAttribList, 0);
if (eglSurface == null) {
EGL14.eglDestroyContext(eglDisplay, eglContext);
EGL14.eglTerminate(eglDisplay);
throw new OpenGLException("Failed to create EGL window surface");
}
if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
EGL14.eglDestroySurface(eglDisplay, eglSurface);
EGL14.eglDestroyContext(eglDisplay, eglContext);
EGL14.eglTerminate(eglDisplay);
throw new OpenGLException("Failed to make EGL context current");
}
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
GLUtils.checkGlError();
textureId = textures[0];
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLUtils.checkGlError();
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLUtils.checkGlError();
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.checkGlError();
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLUtils.checkGlError();
surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setDefaultBufferSize(inputSize.getWidth(), inputSize.getHeight());
inputSurface = new Surface(surfaceTexture);
filter.init();
surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
if (stopped) {
// Make sure to never render after resources have been released
return;
}
render(outputSize);
}, handler);
}
private void render(Size outputSize) {
GLES20.glViewport(0, 0, outputSize.getWidth(), outputSize.getHeight());
GLUtils.checkGlError();
surfaceTexture.updateTexImage();
float[] matrix;
if (overrideTransformMatrix != null) {
matrix = overrideTransformMatrix;
} else {
matrix = new float[16];
surfaceTexture.getTransformMatrix(matrix);
}
filter.draw(textureId, matrix);
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTexture.getTimestamp());
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
}
public void stopAndRelease() {
final Semaphore sem = new Semaphore(0);
handler.post(() -> {
stopped = true;
surfaceTexture.setOnFrameAvailableListener(null, handler);
filter.release();
int[] textures = {textureId};
GLES20.glDeleteTextures(1, textures, 0);
GLUtils.checkGlError();
EGL14.eglDestroySurface(eglDisplay, eglSurface);
EGL14.eglDestroyContext(eglDisplay, eglContext);
EGL14.eglTerminate(eglDisplay);
eglDisplay = EGL14.EGL_NO_DISPLAY;
eglContext = EGL14.EGL_NO_CONTEXT;
eglSurface = EGL14.EGL_NO_SURFACE;
surfaceTexture.release();
inputSurface.release();
sem.release();
});
try {
sem.acquire();
} catch (InterruptedException e) {
// Behave as if this method call was synchronous
Thread.currentThread().interrupt();
}
}
}

View File

@ -0,0 +1,368 @@
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Size;
/**
* Represents a 2D affine transform (a 3x3 matrix):
*
* <pre>
* / a c e \
* | b d f |
* \ 0 0 1 /
* </pre>
* <p>
* Or, a 4x4 matrix if we add a z axis:
*
* <pre>
* / a c 0 e \
* | b d 0 f |
* | 0 0 1 0 |
* \ 0 0 0 1 /
* </pre>
*/
public class AffineMatrix {
private final double a, b, c, d, e, f;
/**
* The identity matrix.
*/
public static final AffineMatrix IDENTITY = new AffineMatrix(1, 0, 0, 1, 0, 0);
/**
* Create a new matrix:
*
* <pre>
* / a c e \
* | b d f |
* \ 0 0 1 /
* </pre>
*/
public AffineMatrix(double a, double b, double c, double d, double e, double f) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
this.e = e;
this.f = f;
}
@Override
public String toString() {
return "[" + a + ", " + c + ", " + e + "; " + b + ", " + d + ", " + f + "]";
}
/**
* Return a matrix which converts from Normalized Device Coordinates to pixels.
*
* @param size the target size
* @return the transform matrix
*/
public static AffineMatrix ndcFromPixels(Size size) {
double w = size.getWidth();
double h = size.getHeight();
return new AffineMatrix(1 / w, 0, 0, -1 / h, 0, 1);
}
/**
* Return a matrix which converts from pixels to Normalized Device Coordinates.
*
* @param size the source size
* @return the transform matrix
*/
public static AffineMatrix ndcToPixels(Size size) {
double w = size.getWidth();
double h = size.getHeight();
return new AffineMatrix(w, 0, 0, -h, 0, h);
}
/**
* Apply the transform to a point ({@code this} should be a matrix converted to pixels coordinates via {@link #ndcToPixels(Size)}).
*
* @param point the source point
* @return the converted point
*/
public Point apply(Point point) {
int x = point.getX();
int y = point.getY();
int xx = (int) (a * x + c * y + e);
int yy = (int) (b * x + d * y + f);
return new Point(xx, yy);
}
/**
* Compute <code>this * rhs</code>.
*
* @param rhs the matrix to multiply
* @return the product
*/
public AffineMatrix multiply(AffineMatrix rhs) {
if (rhs == null) {
// For convenience
return this;
}
double aa = this.a * rhs.a + this.c * rhs.b;
double bb = this.b * rhs.a + this.d * rhs.b;
double cc = this.a * rhs.c + this.c * rhs.d;
double dd = this.b * rhs.c + this.d * rhs.d;
double ee = this.a * rhs.e + this.c * rhs.f + this.e;
double ff = this.b * rhs.e + this.d * rhs.f + this.f;
return new AffineMatrix(aa, bb, cc, dd, ee, ff);
}
/**
* Multiply all matrices from left to right, ignoring any {@code null} matrix (for convenience).
*
* @param matrices the matrices
* @return the product
*/
public static AffineMatrix multiplyAll(AffineMatrix... matrices) {
AffineMatrix result = null;
for (AffineMatrix matrix : matrices) {
if (result == null) {
result = matrix;
} else {
result = result.multiply(matrix);
}
}
return result;
}
/**
* Invert the matrix.
*
* @return the inverse matrix (or {@code null} if not invertible).
*/
public AffineMatrix invert() {
// The 3x3 matrix M can be decomposed into M = M1 * M2:
// M1 M2
// / 1 0 e \ / a c 0 \
// | 0 1 f | * | b d 0 |
// \ 0 0 1 / \ 0 0 1 /
//
// The inverse of an invertible 2x2 matrix is given by this formula:
//
// / A B \⁻¹ 1 / D -B \
// \ C D / = ----- \ -C A /
// AD-BC
//
// Let B=c and C=b (to apply the general formula with the same letters).
//
// M⁻¹ = (M1 * M2)⁻¹ = M2⁻¹ * M1⁻¹
//
// M2⁻¹ M1⁻¹
// /----------------\
// 1 / d -B 0 \ / 1 0 -e \
// = ----- | -C a 0 | * | 0 1 -f |
// ad-BC \ 0 0 1 / \ 0 0 1 /
//
// With the original letters:
//
// 1 / d -c 0 \ / 1 0 -e \
// M⁻¹ = ----- | -b a 0 | * | 0 1 -f |
// ad-cb \ 0 0 1 / \ 0 0 1 /
//
// 1 / d -c cf-de \
// = ----- | -b a be-af |
// ad-cb \ 0 0 1 /
double det = a * d - c * b;
if (det == 0) {
// Not invertible
return null;
}
double aa = d / det;
double bb = -b / det;
double cc = -c / det;
double dd = a / det;
double ee = (c * f - d * e) / det;
double ff = (b * e - a * f) / det;
return new AffineMatrix(aa, bb, cc, dd, ee, ff);
}
/**
* Return this transform applied from the center (0.5, 0.5).
*
* @return the resulting matrix
*/
public AffineMatrix fromCenter() {
return translate(0.5, 0.5).multiply(this).multiply(translate(-0.5, -0.5));
}
/**
* Return this transform with the specified aspect ratio.
*
* @param ar the aspect ratio
* @return the resulting matrix
*/
public AffineMatrix withAspectRatio(double ar) {
return scale(1 / ar, 1).multiply(this).multiply(scale(ar, 1));
}
/**
* Return this transform with the specified aspect ratio.
*
* @param size the size describing the aspect ratio
* @return the transform
*/
public AffineMatrix withAspectRatio(Size size) {
double ar = (double) size.getWidth() / size.getHeight();
return withAspectRatio(ar);
}
/**
* Return a translation matrix.
*
* @param x the horizontal translation
* @param y the vertical translation
* @return the matrix
*/
public static AffineMatrix translate(double x, double y) {
return new AffineMatrix(1, 0, 0, 1, x, y);
}
/**
* Return a scaling matrix.
*
* @param x the horizontal scaling
* @param y the vertical scaling
* @return the matrix
*/
public static AffineMatrix scale(double x, double y) {
return new AffineMatrix(x, 0, 0, y, 0, 0);
}
/**
* Return a scaling matrix.
*
* @param from the source size
* @param to the destination size
* @return the matrix
*/
public static AffineMatrix scale(Size from, Size to) {
double scaleX = (double) to.getWidth() / from.getWidth();
double scaleY = (double) to.getHeight() / from.getHeight();
return scale(scaleX, scaleY);
}
/**
* Return a matrix applying a "reframing" (cropping a rectangle).
* <p/>
* <code>(x, y)</code> is the bottom-left corner, <code>(w, h)</code> is the size of the rectangle.
*
* @param x horizontal coordinate (increasing to the right)
* @param y vertical coordinate (increasing upwards)
* @param w width
* @param h height
* @return the matrix
*/
public static AffineMatrix reframe(double x, double y, double w, double h) {
if (w == 0 || h == 0) {
throw new IllegalArgumentException("Cannot reframe to an empty area: " + w + "x" + h);
}
return scale(1 / w, 1 / h).multiply(translate(-x, -y));
}
/**
* Return an orthogonal rotation matrix.
*
* @param ccwRotation the counter-clockwise rotation
* @return the matrix
*/
public static AffineMatrix rotateOrtho(int ccwRotation) {
switch (ccwRotation) {
case 0:
return IDENTITY;
case 1:
// 90° counter-clockwise
return new AffineMatrix(0, 1, -1, 0, 1, 0);
case 2:
// 180°
return new AffineMatrix(-1, 0, 0, -1, 1, 1);
case 3:
// 90° clockwise
return new AffineMatrix(0, -1, 1, 0, 0, 1);
default:
throw new IllegalArgumentException("Invalid rotation: " + ccwRotation);
}
}
/**
* Return an horizontal flip matrix.
*
* @return the matrix
*/
public static AffineMatrix hflip() {
return new AffineMatrix(-1, 0, 0, 1, 1, 0);
}
/**
* Return a vertical flip matrix.
*
* @return the matrix
*/
public static AffineMatrix vflip() {
return new AffineMatrix(1, 0, 0, -1, 0, 1);
}
/**
* Return a rotation matrix.
*
* @param ccwDegrees the angle, in degrees (counter-clockwise)
* @return the matrix
*/
public static AffineMatrix rotate(double ccwDegrees) {
double radians = Math.toRadians(ccwDegrees);
double cos = Math.cos(radians);
double sin = Math.sin(radians);
return new AffineMatrix(cos, sin, -sin, cos, 0, 0);
}
/**
* Export this affine transform to a 4x4 column-major order matrix.
*
* @param matrix output 4x4 matrix
*/
public void to4x4(float[] matrix) {
// matrix is a 4x4 matrix in column-major order
// Column 0
matrix[0] = (float) a;
matrix[1] = (float) b;
matrix[2] = 0;
matrix[3] = 0;
// Column 1
matrix[4] = (float) c;
matrix[5] = (float) d;
matrix[6] = 0;
matrix[7] = 0;
// Column 2
matrix[8] = 0;
matrix[9] = 0;
matrix[10] = 1;
matrix[11] = 0;
// Column 3
matrix[12] = (float) e;
matrix[13] = (float) f;
matrix[14] = 0;
matrix[15] = 1;
}
/**
* Export this affine transform to a 4x4 column-major order matrix.
*
* @return 4x4 matrix
*/
public float[] to4x4() {
float[] matrix = new float[16];
to4x4(matrix);
return matrix;
}
}

View File

@ -236,7 +236,7 @@ public final class LogUtils {
} else { } else {
builder.append("\n ").append(String.format("%" + column + "s", " ")); builder.append("\n ").append(String.format("%" + column + "s", " "));
} }
builder.append(" [").append(app.getPackageName()).append(']'); builder.append(" ").append(app.getPackageName());
} }
return builder.toString(); return builder.toString();

View File

@ -1,9 +1,17 @@
package com.genymobile.scrcpy.video; package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
@ -38,6 +46,13 @@ import java.util.stream.Stream;
public class CameraCapture extends SurfaceCapture { public class CameraCapture extends SurfaceCapture {
public static final float[] VFLIP_MATRIX = {
1, 0, 0, 0, // column 1
0, -1, 0, 0, // column 2
0, 0, 1, 0, // column 3
0, 1, 0, 1, // column 4
};
private final String explicitCameraId; private final String explicitCameraId;
private final CameraFacing cameraFacing; private final CameraFacing cameraFacing;
private final Size explicitSize; private final Size explicitSize;
@ -45,9 +60,16 @@ public class CameraCapture extends SurfaceCapture {
private final CameraAspectRatio aspectRatio; private final CameraAspectRatio aspectRatio;
private final int fps; private final int fps;
private final boolean highSpeed; private final boolean highSpeed;
private final Rect crop;
private final Orientation captureOrientation;
private final float angle;
private String cameraId; private String cameraId;
private Size size; private Size captureSize;
private Size videoSize; // after OpenGL transforms
private AffineMatrix transform;
private OpenGLRunner glRunner;
private HandlerThread cameraThread; private HandlerThread cameraThread;
private Handler cameraHandler; private Handler cameraHandler;
@ -56,19 +78,22 @@ public class CameraCapture extends SurfaceCapture {
private final AtomicBoolean disconnected = new AtomicBoolean(); private final AtomicBoolean disconnected = new AtomicBoolean();
public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, public CameraCapture(Options options) {
boolean highSpeed) { this.explicitCameraId = options.getCameraId();
this.explicitCameraId = explicitCameraId; this.cameraFacing = options.getCameraFacing();
this.cameraFacing = cameraFacing; this.explicitSize = options.getCameraSize();
this.explicitSize = explicitSize; this.maxSize = options.getMaxSize();
this.maxSize = maxSize; this.aspectRatio = options.getCameraAspectRatio();
this.aspectRatio = aspectRatio; this.fps = options.getCameraFps();
this.fps = fps; this.highSpeed = options.getCameraHighSpeed();
this.highSpeed = highSpeed; this.crop = options.getCrop();
this.captureOrientation = options.getCaptureOrientation();
assert captureOrientation != null;
this.angle = options.getAngle();
} }
@Override @Override
public void init() throws IOException { protected void init() throws ConfigurationException, IOException {
cameraThread = new HandlerThread("camera"); cameraThread = new HandlerThread("camera");
cameraThread.start(); cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper()); cameraHandler = new Handler(cameraThread.getLooper());
@ -77,12 +102,7 @@ public class CameraCapture extends SurfaceCapture {
try { try {
cameraId = selectCamera(explicitCameraId, cameraFacing); cameraId = selectCamera(explicitCameraId, cameraFacing);
if (cameraId == null) { if (cameraId == null) {
throw new IOException("No matching camera found"); throw new ConfigurationException("No matching camera found");
}
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (size == null) {
throw new IOException("Could not select camera size");
} }
Ln.i("Using camera '" + cameraId + "'"); Ln.i("Using camera '" + cameraId + "'");
@ -92,14 +112,45 @@ public class CameraCapture extends SurfaceCapture {
} }
} }
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { @Override
if (explicitCameraId != null) { public void prepare() throws IOException {
return explicitCameraId; try {
captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (captureSize == null) {
throw new IOException("Could not select camera size");
}
} catch (CameraAccessException e) {
throw new IOException(e);
} }
VideoFilter filter = new VideoFilter(captureSize);
if (crop != null) {
filter.addCrop(crop, false);
}
if (captureOrientation != Orientation.Orient0) {
filter.addOrientation(captureOrientation);
}
filter.addAngle(angle);
transform = filter.getInverseTransform();
videoSize = filter.getOutputSize().limit(maxSize).round8();
}
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException {
CameraManager cameraManager = ServiceManager.getCameraManager(); CameraManager cameraManager = ServiceManager.getCameraManager();
String[] cameraIds = cameraManager.getCameraIdList(); String[] cameraIds = cameraManager.getCameraIdList();
if (explicitCameraId != null) {
if (!Arrays.asList(cameraIds).contains(explicitCameraId)) {
Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false));
throw new ConfigurationException("Camera id not found");
}
return explicitCameraId;
}
if (cameraFacing == null) { if (cameraFacing == null) {
// Use the first one // Use the first one
return cameraIds.length > 0 ? cameraIds[0] : null; return cameraIds.length > 0 ? cameraIds[0] : null;
@ -201,15 +252,33 @@ public class CameraCapture extends SurfaceCapture {
@Override @Override
public void start(Surface surface) throws IOException { public void start(Surface surface) throws IOException {
if (transform != null) {
assert glRunner == null;
OpenGLFilter glFilter = new AffineOpenGLFilter(transform);
// The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90°
// rotation). Use a vertical flip transform matrix instead.
glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX);
surface = glRunner.start(captureSize, videoSize, surface);
}
try { try {
CameraCaptureSession session = createCaptureSession(cameraDevice, surface); CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest request = createCaptureRequest(surface); CaptureRequest request = createCaptureRequest(surface);
setRepeatingRequest(session, request); setRepeatingRequest(session, request);
} catch (CameraAccessException | InterruptedException e) { } catch (CameraAccessException | InterruptedException e) {
stop();
throw new IOException(e); throw new IOException(e);
} }
} }
@Override
public void stop() {
if (glRunner != null) {
glRunner.stopAndRelease();
glRunner = null;
}
}
@Override @Override
public void release() { public void release() {
if (cameraDevice != null) { if (cameraDevice != null) {
@ -222,7 +291,7 @@ public class CameraCapture extends SurfaceCapture {
@Override @Override
public Size getSize() { public Size getSize() {
return size; return videoSize;
} }
@Override @Override
@ -232,13 +301,7 @@ public class CameraCapture extends SurfaceCapture {
} }
this.maxSize = maxSize; this.maxSize = maxSize;
try { return true;
size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed);
return size != null;
} catch (CameraAccessException e) {
Ln.w("Could not select camera size", e);
return false;
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -256,7 +319,7 @@ public class CameraCapture extends SurfaceCapture {
public void onDisconnected(CameraDevice camera) { public void onDisconnected(CameraDevice camera) {
Ln.w("Camera disconnected"); Ln.w("Camera disconnected");
disconnected.set(true); disconnected.set(true);
requestReset(); invalidate();
} }
@Override @Override
@ -355,4 +418,9 @@ public class CameraCapture extends SurfaceCapture {
public boolean isClosed() { public boolean isClosed() {
return disconnected.get(); return disconnected.get();
} }
@Override
public void requestInvalidate() {
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
}
} }

View File

@ -0,0 +1,37 @@
package com.genymobile.scrcpy.video;
import android.media.MediaCodec;
import java.util.concurrent.atomic.AtomicBoolean;
public class CaptureReset implements SurfaceCapture.CaptureListener {
private final AtomicBoolean reset = new AtomicBoolean();
// Current instance of MediaCodec to "interrupt" on reset
private MediaCodec runningMediaCodec;
public boolean consumeReset() {
return reset.getAndSet(false);
}
public synchronized void reset() {
reset.set(true);
if (runningMediaCodec != null) {
try {
runningMediaCodec.signalEndOfInputStream();
} catch (IllegalStateException e) {
// ignore
}
}
}
public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) {
this.runningMediaCodec = runningMediaCodec;
}
@Override
public void onInvalidated() {
reset();
}
}

View File

@ -0,0 +1,139 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.DisplayWindowListener;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.IDisplayWindowListener;
public class DisplaySizeMonitor {
public interface Listener {
void onDisplaySizeChanged();
}
// 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, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead.
private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14;
private DisplayManager.DisplayListenerHandle displayListenerHandle;
private HandlerThread handlerThread;
private IDisplayWindowListener displayWindowListener;
private int displayId = Device.DISPLAY_ID_NONE;
private Size sessionDisplaySize;
private Listener listener;
public void start(int displayId, Listener listener) {
// Once started, the listener and the displayId must never change
assert listener != null;
this.listener = listener;
assert this.displayId == Device.DISPLAY_ID_NONE;
this.displayId = displayId;
if (USE_DEFAULT_METHOD) {
handlerThread = new HandlerThread("DisplayListener");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")");
}
if (eventDisplayId == displayId) {
checkDisplaySizeChanged();
}
}, handler);
} else {
displayWindowListener = new DisplayWindowListener() {
@Override
public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) {
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")");
}
if (eventDisplayId == displayId) {
checkDisplaySizeChanged();
}
}
};
ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener);
}
}
/**
* Stop and release the monitor.
* <p/>
* It must not be used anymore.
* It is ok to call this method even if {@link #start(int, Listener)} was not called.
*/
public void stopAndRelease() {
if (USE_DEFAULT_METHOD) {
// displayListenerHandle may be null if registration failed
if (displayListenerHandle != null) {
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
displayListenerHandle = null;
}
if (handlerThread != null) {
handlerThread.quitSafely();
}
} else if (displayWindowListener != null) {
ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener);
}
}
private synchronized Size getSessionDisplaySize() {
return sessionDisplaySize;
}
public synchronized void setSessionDisplaySize(Size sessionDisplaySize) {
this.sessionDisplaySize = sessionDisplaySize;
}
private void checkDisplaySizeChanged() {
DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (di == null) {
Ln.w("DisplayInfo for " + displayId + " cannot be retrieved");
// We can't compare with the current size, so reset unconditionally
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)");
}
setSessionDisplaySize(null);
listener.onDisplaySizeChanged();
} else {
Size size = di.getSize();
// The field is hidden on purpose, to read it with synchronization
@SuppressWarnings("checkstyle:HiddenField")
Size sessionDisplaySize = getSessionDisplaySize(); // synchronized
// .equals() also works if sessionDisplaySize == null
if (!size.equals(sessionDisplaySize)) {
// Reset only if the size is different
if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size);
}
// Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare()
// considers that the current size is the requested size (to avoid a duplicate requestReset())
setSessionDisplaySize(size);
listener.onDisplaySizeChanged();
} else if (Ln.isEnabled(Ln.Level.VERBOSE)) {
Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()");
}
}
}
}

View File

@ -1,22 +1,31 @@
package com.genymobile.scrcpy.video; package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.Surface;
import java.io.IOException;
public class NewDisplayCapture extends SurfaceCapture { public class NewDisplayCapture extends SurfaceCapture {
// Internal fields copied from android.hardware.display.DisplayManager // Internal fields copied from android.hardware.display.DisplayManager
private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
@ -31,28 +40,53 @@ public class NewDisplayCapture extends SurfaceCapture {
private final VirtualDisplayListener vdListener; private final VirtualDisplayListener vdListener;
private final NewDisplay newDisplay; private final NewDisplay newDisplay;
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
private AffineMatrix displayTransform;
private AffineMatrix eventTransform;
private OpenGLRunner glRunner;
private Size mainDisplaySize; private Size mainDisplaySize;
private int mainDisplayDpi; private int mainDisplayDpi;
private int maxSize; // only used if newDisplay.getSize() != null private int maxSize;
private final Rect crop;
private final boolean captureOrientationLocked;
private final Orientation captureOrientation;
private final float angle;
private final boolean vdSystemDecorations;
private VirtualDisplay virtualDisplay; private VirtualDisplay virtualDisplay;
private Size size; private Size videoSize;
private Size displaySize; // the logical size of the display (including rotation)
private Size physicalSize; // the physical size of the display (without rotation)
private int dpi; private int dpi;
public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) { public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener; this.vdListener = vdListener;
this.newDisplay = newDisplay; this.newDisplay = options.getNewDisplay();
this.maxSize = maxSize; assert newDisplay != null;
this.maxSize = options.getMaxSize();
this.crop = options.getCrop();
assert options.getCaptureOrientationLock() != null;
this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked;
this.captureOrientation = options.getCaptureOrientation();
assert captureOrientation != null;
this.angle = options.getAngle();
this.vdSystemDecorations = options.getVDSystemDecorations();
} }
@Override @Override
public void init() { protected void init() {
size = newDisplay.getSize(); displaySize = newDisplay.getSize();
dpi = newDisplay.getDpi(); dpi = newDisplay.getDpi();
if (size == null || dpi == 0) { if (displaySize == null || dpi == 0) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0);
if (displayInfo != null) { if (displayInfo != null) {
mainDisplaySize = displayInfo.getSize(); mainDisplaySize = displayInfo.getSize();
if ((displayInfo.getRotation() % 2) != 0) {
mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one
}
mainDisplayDpi = displayInfo.getDpi(); mainDisplayDpi = displayInfo.getDpi();
} else { } else {
Ln.w("Main display not found, fallback to 1920x1080 240dpi"); Ln.w("Main display not found, fallback to 1920x1080 240dpi");
@ -64,58 +98,135 @@ public class NewDisplayCapture extends SurfaceCapture {
@Override @Override
public void prepare() { public void prepare() {
if (!newDisplay.hasExplicitSize()) { int displayRotation;
size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize); if (virtualDisplay == null) {
if (!newDisplay.hasExplicitSize()) {
displaySize = mainDisplaySize;
}
if (!newDisplay.hasExplicitDpi()) {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize);
}
videoSize = displaySize;
displayRotation = 0;
// Set the current display size to avoid an unnecessary call to invalidate()
displaySizeMonitor.setSessionDisplaySize(displaySize);
} else {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId());
displaySize = displayInfo.getSize();
dpi = displayInfo.getDpi();
displayRotation = displayInfo.getRotation();
} }
if (!newDisplay.hasExplicitDpi()) {
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); VideoFilter filter = new VideoFilter(displaySize);
if (crop != null) {
boolean transposed = (displayRotation % 2) != 0;
filter.addCrop(crop, transposed);
} }
filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation);
filter.addAngle(angle);
Size filteredSize = filter.getOutputSize();
if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) {
if (maxSize != 0) {
filteredSize = filteredSize.limit(maxSize);
}
filteredSize = filteredSize.round8();
filter.addResize(filteredSize);
}
eventTransform = filter.getInverseTransform();
// DisplayInfo gives the oriented size (so videoSize includes the display rotation)
videoSize = filter.getOutputSize();
// But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually).
// This additional display rotation must not be included in the input events transform (the expected coordinates are already in the
// physical display size)
if ((displayRotation % 2) == 0) {
physicalSize = displaySize;
} else {
physicalSize = displaySize.rotate();
}
VideoFilter displayFilter = new VideoFilter(physicalSize);
displayFilter.addRotation(displayRotation);
AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform();
// Take care of multiplication order:
// displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹
// = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹
// = displayRotationMatrix * eventTransform
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
} }
@Override public void startNew(Surface surface) {
public void start(Surface surface) {
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
int virtualDisplayId; int virtualDisplayId;
try { try {
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
| VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; if (vdSystemDecorations) {
flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) {
flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
| VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
| VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
} }
} }
virtualDisplay = ServiceManager.getDisplayManager() virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); .createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags);
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
displaySizeMonitor.start(virtualDisplayId, this::invalidate);
} catch (Exception e) { } catch (Exception e) {
Ln.e("Could not create display", e); Ln.e("Could not create display", e);
throw new AssertionError("Could not create display"); throw new AssertionError("Could not create display");
} }
}
@Override
public void start(Surface surface) throws IOException {
if (displayTransform != null) {
assert glRunner == null;
OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform);
glRunner = new OpenGLRunner(glFilter);
surface = glRunner.start(physicalSize, videoSize, surface);
}
if (virtualDisplay == null) {
startNew(surface);
} else {
virtualDisplay.setSurface(surface);
}
if (vdListener != null) { if (vdListener != null) {
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize);
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper);
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); }
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); }
@Override
public void stop() {
if (glRunner != null) {
glRunner.stopAndRelease();
glRunner = null;
} }
} }
@Override @Override
public void release() { public void release() {
displaySizeMonitor.stopAndRelease();
if (virtualDisplay != null) { if (virtualDisplay != null) {
virtualDisplay.release(); virtualDisplay.release();
virtualDisplay = null; virtualDisplay = null;
@ -124,16 +235,11 @@ public class NewDisplayCapture extends SurfaceCapture {
@Override @Override
public synchronized Size getSize() { public synchronized Size getSize() {
return size; return videoSize;
} }
@Override @Override
public synchronized boolean setMaxSize(int newMaxSize) { public synchronized boolean setMaxSize(int newMaxSize) {
if (newDisplay.hasExplicitSize()) {
// Cannot retry with a different size if the display size was explicitly provided
return false;
}
maxSize = newMaxSize; maxSize = newMaxSize;
return true; return true;
} }
@ -143,4 +249,9 @@ public class NewDisplayCapture extends SurfaceCapture {
int num = size.getMax(); int num = size.getMax();
return initialDpi * num / den; return initialDpi * num / den;
} }
@Override
public void requestInvalidate() {
invalidate();
}
} }

View File

@ -1,10 +1,17 @@
package com.genymobile.scrcpy.video; package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.opengl.AffineOpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLFilter;
import com.genymobile.scrcpy.opengl.OpenGLRunner;
import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
@ -14,70 +21,47 @@ import android.graphics.Rect;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
import android.view.Surface; import android.view.Surface;
import java.io.IOException;
public class ScreenCapture extends SurfaceCapture { public class ScreenCapture extends SurfaceCapture {
private final VirtualDisplayListener vdListener; private final VirtualDisplayListener vdListener;
private final int displayId; private final int displayId;
private int maxSize; private int maxSize;
private final Rect crop; private final Rect crop;
private final int lockVideoOrientation; private Orientation.Lock captureOrientationLock;
private Orientation captureOrientation;
private final float angle;
private DisplayInfo displayInfo; private DisplayInfo displayInfo;
private ScreenInfo screenInfo; private Size videoSize;
private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor();
private IBinder display; private IBinder display;
private VirtualDisplay virtualDisplay; private VirtualDisplay virtualDisplay;
private IRotationWatcher rotationWatcher; private AffineMatrix transform;
private IDisplayFoldListener displayFoldListener; private OpenGLRunner glRunner;
public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { public ScreenCapture(VirtualDisplayListener vdListener, Options options) {
this.vdListener = vdListener; this.vdListener = vdListener;
this.displayId = displayId; this.displayId = options.getDisplayId();
this.maxSize = maxSize; assert displayId != Device.DISPLAY_ID_NONE;
this.crop = crop; this.maxSize = options.getMaxSize();
this.lockVideoOrientation = lockVideoOrientation; this.crop = options.getCrop();
this.captureOrientationLock = options.getCaptureOrientationLock();
this.captureOrientation = options.getCaptureOrientation();
assert captureOrientationLock != null;
assert captureOrientation != null;
this.angle = options.getAngle();
} }
@Override @Override
public void init() { public void init() {
if (displayId == 0) { displaySizeMonitor.start(displayId, this::invalidate);
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
displayFoldListener = new IDisplayFoldListener.Stub() {
private boolean first = true;
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (first) {
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
first = false;
return;
}
if (ScreenCapture.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
requestReset();
}
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
} }
@Override @Override
@ -92,11 +76,32 @@ public class ScreenCapture extends SurfaceCapture {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
} }
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); Size displaySize = displayInfo.getSize();
displaySizeMonitor.setSessionDisplaySize(displaySize);
if (captureOrientationLock == Orientation.Lock.LockedInitial) {
// The user requested to lock the video orientation to the current orientation
captureOrientationLock = Orientation.Lock.LockedValue;
captureOrientation = Orientation.fromRotation(displayInfo.getRotation());
}
VideoFilter filter = new VideoFilter(displaySize);
if (crop != null) {
boolean transposed = (displayInfo.getRotation() % 2) != 0;
filter.addCrop(crop, transposed);
}
boolean locked = captureOrientationLock != Orientation.Lock.Unlocked;
filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation);
filter.addAngle(angle);
transform = filter.getInverseTransform();
videoSize = filter.getOutputSize().limit(maxSize).round8();
} }
@Override @Override
public void start(Surface surface) { public void start(Surface surface) throws IOException {
if (display != null) { if (display != null) {
SurfaceControl.destroyDisplay(display); SurfaceControl.destroyDisplay(display);
display = null; display = null;
@ -106,31 +111,40 @@ public class ScreenCapture extends SurfaceCapture {
virtualDisplay = null; virtualDisplay = null;
} }
Size inputSize;
if (transform != null) {
// If there is a filter, it must receive the full display content
inputSize = displayInfo.getSize();
assert glRunner == null;
OpenGLFilter glFilter = new AffineOpenGLFilter(transform);
glRunner = new OpenGLRunner(glFilter);
surface = glRunner.start(inputSize, videoSize, surface);
} else {
// If there is no filter, the display must be rendered at target video size directly
inputSize = videoSize;
}
int virtualDisplayId; int virtualDisplayId;
PositionMapper positionMapper; PositionMapper positionMapper;
try { try {
Size videoSize = screenInfo.getVideoSize();
virtualDisplay = ServiceManager.getDisplayManager() virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface);
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight());
// The position are relative to the virtual display, not the original display // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!)
positionMapper = new PositionMapper(videoSize, contentRect, 0); positionMapper = PositionMapper.create(videoSize, transform, inputSize);
Ln.d("Display: using DisplayManager API"); Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) { } catch (Exception displayManagerException) {
try { try {
display = createDisplay(); display = createDisplay();
Rect contentRect = screenInfo.getContentRect(); Size deviceSize = displayInfo.getSize();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = displayInfo.getLayerStack(); int layerStack = displayInfo.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack);
virtualDisplayId = displayId; virtualDisplayId = displayId;
positionMapper = PositionMapper.from(screenInfo);
positionMapper = PositionMapper.create(videoSize, transform, deviceSize);
Ln.d("Display: using SurfaceControl API"); Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) { } catch (Exception surfaceControlException) {
Ln.e("Could not create display using DisplayManager", displayManagerException); Ln.e("Could not create display using DisplayManager", displayManagerException);
@ -144,14 +158,18 @@ public class ScreenCapture extends SurfaceCapture {
} }
} }
@Override
public void stop() {
if (glRunner != null) {
glRunner.stopAndRelease();
glRunner = null;
}
}
@Override @Override
public void release() { public void release() {
if (rotationWatcher != null) { displaySizeMonitor.stopAndRelease();
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
}
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
}
if (display != null) { if (display != null) {
SurfaceControl.destroyDisplay(display); SurfaceControl.destroyDisplay(display);
display = null; display = null;
@ -164,7 +182,7 @@ public class ScreenCapture extends SurfaceCapture {
@Override @Override
public Size getSize() { public Size getSize() {
return screenInfo.getVideoSize(); return videoSize;
} }
@Override @Override
@ -181,14 +199,19 @@ public class ScreenCapture extends SurfaceCapture {
return SurfaceControl.createDisplay("scrcpy", secure); return SurfaceControl.createDisplay("scrcpy", secure);
} }
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction(); SurfaceControl.openTransaction();
try { try {
SurfaceControl.setDisplaySurface(display, surface); SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, layerStack); SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally { } finally {
SurfaceControl.closeTransaction(); SurfaceControl.closeTransaction();
} }
} }
@Override
public void requestInvalidate() {
invalidate();
}
} }

View File

@ -1,149 +0,0 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.BuildConfig;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import android.graphics.Rect;
public final class ScreenInfo {
/**
* Device (physical) size, possibly cropped
*/
private final Rect contentRect; // device size, possibly cropped
/**
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
* <p>
* However, it does not include the locked video orientation.
*/
private final Size unlockedVideoSize;
/**
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
*/
private final int deviceRotation;
/**
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
*/
private final int lockedVideoOrientation;
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
this.contentRect = contentRect;
this.unlockedVideoSize = unlockedVideoSize;
this.deviceRotation = deviceRotation;
this.lockedVideoOrientation = lockedVideoOrientation;
}
public Rect getContentRect() {
return contentRect;
}
/**
* Return the video size as if locked video orientation was not set.
*
* @return the unlocked video size
*/
public Size getUnlockedVideoSize() {
return unlockedVideoSize;
}
/**
* Return the actual video size if locked video orientation is set.
*
* @return the actual video size
*/
public Size getVideoSize() {
if (getVideoRotation() % 2 == 0) {
return unlockedVideoSize;
}
return unlockedVideoSize.rotate();
}
public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) {
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
// The user requested to lock the video orientation to the current orientation
lockedVideoOrientation = rotation;
}
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotation % 2 != 0) { // 180s preserve dimensions
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
public static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
}
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maxSize) {
int minorExact = minor * maxSize / major;
// +4 to round the value to the nearest multiple of 8
minor = (minorExact + 4) & ~7;
major = maxSize;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new Size(w, h);
}
private static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
/**
* Return the rotation to apply to the device rotation to get the requested locked video orientation
*
* @return the rotation offset
*/
public int getVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
}
/**
* Return the rotation to apply to the requested locked video orientation to get the device rotation
*
* @return the (reverse) rotation offset
*/
public int getReverseVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
}
}

View File

@ -6,36 +6,37 @@ import com.genymobile.scrcpy.device.Size;
import android.view.Surface; import android.view.Surface;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* A video source which can be rendered on a Surface for encoding. * A video source which can be rendered on a Surface for encoding.
*/ */
public abstract class SurfaceCapture { public abstract class SurfaceCapture {
private final AtomicBoolean resetCapture = new AtomicBoolean(); public interface CaptureListener {
void onInvalidated();
/**
* Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on
* device rotation for example).
*/
protected void requestReset() {
resetCapture.set(true);
} }
private CaptureListener listener;
/** /**
* Consume the reset request (intended to be called by the encoder). * Notify the listener that the capture has been invalidated (for example, because its size changed).
*
* @return {@code true} if a reset request was pending, {@code false} otherwise.
*/ */
public boolean consumeReset() { protected void invalidate() {
return resetCapture.getAndSet(false); listener.onInvalidated();
} }
/** /**
* Called once before the first capture starts. * Called once before the first capture starts.
*/ */
public abstract void init() throws ConfigurationException, IOException; public final void init(CaptureListener listener) throws ConfigurationException, IOException {
this.listener = listener;
init();
}
/**
* Called once before the first capture starts.
*/
protected abstract void init() throws ConfigurationException, IOException;
/** /**
* Called after the last capture ends (if and only if {@link #init()} has been called). * Called after the last capture ends (if and only if {@link #init()} has been called).
@ -45,7 +46,7 @@ public abstract class SurfaceCapture {
/** /**
* Called once before each capture starts, before {@link #getSize()}. * Called once before each capture starts, before {@link #getSize()}.
*/ */
public void prepare() throws ConfigurationException { public void prepare() throws ConfigurationException, IOException {
// empty by default // empty by default
} }
@ -56,6 +57,13 @@ public abstract class SurfaceCapture {
*/ */
public abstract void start(Surface surface) throws IOException; public abstract void start(Surface surface) throws IOException;
/**
* Stop the capture.
*/
public void stop() {
// Do nothing by default
}
/** /**
* Return the video size * Return the video size
* *
@ -78,4 +86,11 @@ public abstract class SurfaceCapture {
public boolean isClosed() { public boolean isClosed() {
return false; return false;
} }
/**
* Manually request to invalidate (typically a user request).
* <p>
* The capture implementation is free to ignore the request and do nothing.
*/
public abstract void requestInvalidate();
} }

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.device.Streamer;
@ -49,15 +50,16 @@ public class SurfaceEncoder implements AsyncProcessor {
private Thread thread; private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean(); private final AtomicBoolean stopped = new AtomicBoolean();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List<CodecOption> codecOptions, private final CaptureReset reset = new CaptureReset();
String encoderName, boolean downsizeOnError) {
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) {
this.capture = capture; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.videoBitRate = videoBitRate; this.videoBitRate = options.getVideoBitRate();
this.maxFps = maxFps; this.maxFps = options.getMaxFps();
this.codecOptions = codecOptions; this.codecOptions = options.getVideoCodecOptions();
this.encoderName = encoderName; this.encoderName = options.getVideoEncoder();
this.downsizeOnError = downsizeOnError; this.downsizeOnError = options.getDownsizeOnError();
} }
private void streamCapture() throws IOException, ConfigurationException { private void streamCapture() throws IOException, ConfigurationException {
@ -65,13 +67,14 @@ public class SurfaceEncoder implements AsyncProcessor {
MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
capture.init(); capture.init(reset);
try { try {
boolean alive; boolean alive;
boolean headerWritten = false; boolean headerWritten = false;
do { do {
reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
capture.prepare(); capture.prepare();
Size size = capture.getSize(); Size size = capture.getSize();
if (!headerWritten) { if (!headerWritten) {
@ -83,25 +86,50 @@ public class SurfaceEncoder implements AsyncProcessor {
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
Surface surface = null; Surface surface = null;
boolean mediaCodecStarted = false;
boolean captureStarted = false;
try { try {
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface(); surface = mediaCodec.createInputSurface();
capture.start(surface); capture.start(surface);
captureStarted = true;
mediaCodec.start(); mediaCodec.start();
mediaCodecStarted = true;
alive = encode(mediaCodec, streamer); // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset
// do not call stop() on exception, it would trigger an IllegalStateException reset.setRunningMediaCodec(mediaCodec);
mediaCodec.stop();
if (stopped.get()) {
alive = false;
} else {
boolean resetRequested = reset.consumeReset();
if (!resetRequested) {
// If a reset is requested during encode(), it will interrupt the encoding by an EOS
encode(mediaCodec, streamer);
}
// The capture might have been closed internally (for example if the camera is disconnected)
alive = !stopped.get() && !capture.isClosed();
}
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareRetry(size)) { if (!prepareRetry(size)) {
throw e; throw e;
} }
Ln.i("Retrying...");
alive = true; alive = true;
} finally { } finally {
reset.setRunningMediaCodec(null);
if (captureStarted) {
capture.stop();
}
if (mediaCodecStarted) {
try {
mediaCodec.stop();
} catch (IllegalStateException e) {
// ignore (just in case)
}
}
mediaCodec.reset(); mediaCodec.reset();
if (surface != null) { if (surface != null) {
surface.release(); surface.release();
@ -162,25 +190,16 @@ public class SurfaceEncoder implements AsyncProcessor {
return 0; return 0;
} }
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { private void encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!capture.consumeReset() && !eof) { boolean eos;
if (stopped.get()) { do {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try { try {
if (capture.consumeReset()) { eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
// must restart encoding with new size // On EOS, there might be data or not, depending on bufferInfo.size
break; if (outputBufferId >= 0 && bufferInfo.size > 0) {
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
@ -197,14 +216,7 @@ public class SurfaceEncoder implements AsyncProcessor {
codec.releaseOutputBuffer(outputBufferId, false); codec.releaseOutputBuffer(outputBufferId, false);
} }
} }
} } while (!eos);
if (capture.isClosed()) {
// The capture might have been closed internally (for example if the camera is disconnected)
alive = false;
}
return !eof && alive;
} }
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@ -297,6 +309,7 @@ public class SurfaceEncoder implements AsyncProcessor {
public void stop() { public void stop() {
if (thread != null) { if (thread != null) {
stopped.set(true); stopped.set(true);
reset.reset();
} }
} }

View File

@ -0,0 +1,119 @@
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Orientation;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.AffineMatrix;
import android.graphics.Rect;
public class VideoFilter {
private Size size;
private AffineMatrix transform;
public VideoFilter(Size inputSize) {
this.size = inputSize;
}
public Size getOutputSize() {
return size;
}
public AffineMatrix getTransform() {
return transform;
}
/**
* Return the inverse transform.
* <p/>
* The direct affine transform describes how the input image is transformed.
* <p/>
* It is often useful to retrieve the inverse transform instead:
* <ul>
* <li>The OpenGL filter expects the matrix to transform the image <em>coordinates</em>, which is the inverse transform;</li>
* <li>The click positions must be transformed back to the device positions, using the inverse transform too.</li>
* </ul>
*
* @return the inverse transform
*/
public AffineMatrix getInverseTransform() {
if (transform == null) {
return null;
}
return transform.invert();
}
private static Rect transposeRect(Rect rect) {
return new Rect(rect.top, rect.left, rect.bottom, rect.right);
}
public void addCrop(Rect crop, boolean transposed) {
if (transposed) {
crop = transposeRect(crop);
}
double inputWidth = size.getWidth();
double inputHeight = size.getHeight();
if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) {
throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")");
}
double x = crop.left / inputWidth;
double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left
double w = crop.width() / inputWidth;
double h = crop.height() / inputHeight;
transform = AffineMatrix.reframe(x, y, w, h).multiply(transform);
size = new Size(crop.width(), crop.height());
}
public void addRotation(int ccwRotation) {
if (ccwRotation == 0) {
return;
}
transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform);
if (ccwRotation % 2 != 0) {
size = size.rotate();
}
}
public void addOrientation(Orientation captureOrientation) {
if (captureOrientation.isFlipped()) {
transform = AffineMatrix.hflip().multiply(transform);
}
int ccwRotation = (4 - captureOrientation.getRotation()) % 4;
addRotation(ccwRotation);
}
public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) {
if (locked) {
// flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0)
int reverseDisplayRotation = (4 - displayRotation) % 4;
addRotation(reverseDisplayRotation);
}
addOrientation(captureOrientation);
}
public void addAngle(double cwAngle) {
if (cwAngle == 0) {
return;
}
double ccwAngle = -cwAngle;
transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform);
}
public void addResize(Size targetSize) {
if (size.equals(targetSize)) {
return;
}
if (transform == null) {
// The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if
// resizing is not performed by the shader. So transform MUST NOT be null.
transform = AffineMatrix.IDENTITY;
}
size = targetSize;
}
}

View File

@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.IContentProvider;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle; import android.os.Bundle;
@ -64,7 +65,7 @@ public final class ActivityManager {
} }
@TargetApi(AndroidVersions.API_29_ANDROID_10) @TargetApi(AndroidVersions.API_29_ANDROID_10)
private ContentProvider getContentProviderExternal(String name, IBinder token) { public IContentProvider getContentProviderExternal(String name, IBinder token) {
try { try {
Method method = getGetContentProviderExternalMethod(); Method method = getGetContentProviderExternalMethod();
Object[] args; Object[] args;
@ -83,11 +84,7 @@ public final class ActivityManager {
// IContentProvider provider = providerHolder.provider; // IContentProvider provider = providerHolder.provider;
Field providerField = providerHolder.getClass().getDeclaredField("provider"); Field providerField = providerHolder.getClass().getDeclaredField("provider");
providerField.setAccessible(true); providerField.setAccessible(true);
Object provider = providerField.get(providerHolder); return (IContentProvider) providerField.get(providerHolder);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, name, token);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
@ -104,7 +101,12 @@ public final class ActivityManager {
} }
public ContentProvider createSettingsProvider() { public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder()); IBinder token = new Binder();
IContentProvider provider = getContentProviderExternal("settings", token);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, "settings", token);
} }
private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException {

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Size;
@ -7,21 +8,46 @@ import com.genymobile.scrcpy.util.Command;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.os.Handler;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@SuppressLint("PrivateApi,DiscouragedPrivateApi") @SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class DisplayManager { public final class DisplayManager {
// android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2;
public interface DisplayListener {
/**
* Called whenever the properties of a logical {@link android.view.Display},
* such as size and density, have changed.
*
* @param displayId The id of the logical display that changed.
*/
void onDisplayChanged(int displayId);
}
public static final class DisplayListenerHandle {
private final Object displayListenerProxy;
private DisplayListenerHandle(Object displayListenerProxy) {
this.displayListenerProxy = displayListenerProxy;
}
}
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
private Method createVirtualDisplayMethod; private Method createVirtualDisplayMethod;
private Method requestDisplayPowerMethod;
static DisplayManager create() { static DisplayManager create() {
try { try {
@ -137,4 +163,71 @@ public final class DisplayManager {
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
} }
private Method getRequestDisplayPowerMethod() throws NoSuchMethodException {
if (requestDisplayPowerMethod == null) {
requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class);
}
return requestDisplayPowerMethod;
}
@TargetApi(AndroidVersions.API_35_ANDROID_15)
public boolean requestDisplayPower(int displayId, boolean on) {
try {
Method method = getRequestDisplayPowerMethod();
return (boolean) method.invoke(manager, displayId, on);
} catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) {
try {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
Object displayListenerProxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[] {displayListenerClass},
(proxy, method, args) -> {
if ("onDisplayChanged".equals(method.getName())) {
listener.onDisplayChanged((int) args[0]);
}
if ("toString".equals(method.getName())) {
return "DisplayListener";
}
return null;
});
try {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class)
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME);
} catch (NoSuchMethodException e) {
try {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class)
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED);
} catch (NoSuchMethodException e2) {
manager.getClass()
.getMethod("registerDisplayListener", displayListenerClass, Handler.class)
.invoke(manager, displayListenerProxy, handler);
}
}
return new DisplayListenerHandle(displayListenerProxy);
} catch (Exception e) {
// Rotation and screen size won't be updated, not a fatal error
Ln.e("Could not register display listener", e);
}
return null;
}
public void unregisterDisplayListener(DisplayListenerHandle listener) {
try {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy);
} catch (Exception e) {
Ln.e("Could not unregister display listener", e);
}
}
} }

View File

@ -0,0 +1,39 @@
package com.genymobile.scrcpy.wrappers;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.view.IDisplayWindowListener;
import java.util.List;
public class DisplayWindowListener extends IDisplayWindowListener.Stub {
@Override
public void onDisplayAdded(int displayId) {
// empty default implementation
}
@Override
public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) {
// empty default implementation
}
@Override
public void onDisplayRemoved(int displayId) {
// empty default implementation
}
@Override
public void onFixedRotationStarted(int displayId, int newRotation) {
// empty default implementation
}
@Override
public void onFixedRotationFinished(int displayId) {
// empty default implementation
}
@Override
public void onKeepClearAreasChanged(int displayId, List<Rect> restricted, List<Rect> unrestricted) {
// empty default implementation
}
}

View File

@ -1,7 +1,9 @@
package com.genymobile.scrcpy.wrappers; package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.IInterface; import android.os.IInterface;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -21,14 +23,22 @@ public final class PowerManager {
private Method getIsScreenOnMethod() throws NoSuchMethodException { private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) { if (isScreenOnMethod == null) {
isScreenOnMethod = manager.getClass().getMethod("isInteractive"); if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class);
} else {
isScreenOnMethod = manager.getClass().getMethod("isInteractive");
}
} }
return isScreenOnMethod; return isScreenOnMethod;
} }
public boolean isScreenOn() { public boolean isScreenOn(int displayId) {
try { try {
Method method = getIsScreenOnMethod(); Method method = getIsScreenOnMethod();
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
return (boolean) method.invoke(manager, displayId);
}
return (boolean) method.invoke(manager); return (boolean) method.invoke(manager);
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);

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