Compare commits

...

76 Commits
rtmp ... v2.6

Author SHA1 Message Date
52136268ef Bump version to 2.6 2024-08-01 18:15:59 +02:00
0a6ccdc4df Merge branch 'master' into release 2024-08-01 18:15:40 +02:00
5d2441d198 Upgrade SDL (2.30.5) for Windows 2024-08-01 18:15:37 +02:00
2b6089cbfc Enable workarounds by default
Workarounds were disabled by default, and only enabled for some devices
or under specific conditions.

But it seems they are needed for more and more devices, so enable them
by default. They could be disabled for specific devices if necessary in
the future.

In the past, these workarounds caused a (harmless) exception to be
printed on some Xiaomi devices [1]. But this is not a problem anymore
since commit b8c5853aa6.

They also caused problems for audio on Vivo devices [2], but it seems
this is not the case anymore [3].

They might also impact an old Nvidia Shield [4], but hopefully this is
fixed now.

[1]: <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
[2]: <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
[3]: <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-2260205882>
[4]: <https://github.com/Genymobile/scrcpy/issues/940>

PR #5154 <https://github.com/Genymobile/scrcpy/pull/5154>
2024-08-01 12:16:35 +02:00
f691ebb1b4 Add workaround for TCL Android 12 Smart TVs
Fixes #5140 <https://github.com/Genymobile/scrcpy/issues/5140>
PR #5148 <https://github.com/Genymobile/scrcpy/pull/5148>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-31 14:55:00 +02:00
071d459ad7 Fix --no-audio
By default, the audio source is initialized to SC_AUDIO_SOURCE_AUTO, and
is "resolved" only if audio is enabled.

But the server arguments were built assuming that the audio source was
never SC_AUDIO_SOURCE_AUTO (even with audio disabled), causing a crash.

Regression introduced by a10f8cd798.
2024-07-29 20:03:44 +02:00
bbfac9ae1f Add FUNDING.yml
The donation links were already in the README.

Also add them in the format expected by GitHub in FUNDING.yml.
2024-07-19 17:56:26 +02:00
65bd6bd8d4 Explicitly accept issues for general questions
Add an empty question template, and reword the "Contact" section in the
README.

Refs #5117 <https://github.com/Genymobile/scrcpy/issues/5117>
2024-07-19 17:51:50 +02:00
ed4066902d Update documentation for audio playback capture
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
127a271d34 Switch audio source if audio-dup is set
Automatically switch implicit audio source to "playback" if --audio-dup
is passed.

This allows to run:

    scrcpy --audio-dup

without specifying explicitly:

    scrcpy --audio-source=playback --audio-dup

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
31116a60d7 Add --audio-dup
Add an option to duplicate audio on the device, compatible with the new
audio playback capture (--audio-source=playback).

Fixes #3875 <https://github.com/Genymobile/scrcpy/issues/3875>
Fixes #4380 <https://github.com/Genymobile/scrcpy/issues/4380>
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-07-19 17:48:39 +02:00
a10f8cd798 Add audio playback capture method
Add a new method to capture audio playback.

It requires Android 13 (where the Shell app has MODIFY_AUDIO_ROUTING
permission).

The main benefit is that it supports keeping audio playing on the device
(implemented in a further commit).

Fixes #4380 <https://github.com/Genymobile/scrcpy/issues/4380>
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-07-19 17:48:39 +02:00
53c6eb66ea Move audio source value
The MediaRecorder constant should not belong to the AudioSource enum.

This will allow to add a new AudioSource which has no meaningful
MediaRecorder audio source value.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
0f076083e8 Extract AudioCapture interface
Move the implementation to AudioDirectCapture and extract an
AudioCapture interface.

This will allow to provide another AudioCapture implementation.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
053bf83f58 Extract AudioRecordReader
Move the logic to read from an AudioRecord and handle all corner cases
for PTS. This simplifies AudioCapture.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
414ce4c754 Move createAudioFormat() to AudioConfig
This will allow to reuse this method.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
a2f3a5cf18 Move hardcoded audio configuration to AudioConfig
This will allow to use these constants from different classes not
directly related to AudioCapture.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
5e605b9b8f Move audio compatibility check
The compatibility depends on the capture constraints, not the encoding.

This will allow to add a new capture implementation with different
constraints.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
cf09e78323 Throw AudioCaptureException on workaround error
Replace a RuntimeException by a specific AudioCaptureException.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
3b8ec0c38d Rename audio capture exception
The AudioCaptureForegroundException was very specific. Rename it to
AudioCaptureException to support other capture failures.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
39132ff2dd Make encode() method private
It is only used from AudioEncoder.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
9d1d79b004 Fix "turn screen off" for Honor Android 14 devices
Fixes #4823 <https://github.com/Genymobile/scrcpy/issues/4823>
PR #5109 <https://github.com/Genymobile/scrcpy/pull/5109>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-17 18:02:29 +02:00
e0cdc2ace3 Fix method name
The method indicates whether GetPhysicalDisplayIds() exists. The "Get"
was missing.
2024-07-17 18:02:26 +02:00
bbcd763612 Exclude install-release tags from git describe
The install_release.sh script is updated one commit after the release
tag, which may be confusing.

For convenience, new lightweight tags have been added (for example
v2.5-install-release) to point to the commit where install_release.sh is
updated.

But these tags interfere with "git describe" to generate pretty
filenames when executing ./release.sh on a development branch, so ignore
them.

Before:

    release-v2.5-install-release-17-gc57a0512b

After:

    release-v2.5-18-gc57a0512b

Refs #4098 comment <https://github.com/Genymobile/scrcpy/issues/4098#issuecomment-1600332180>
2024-07-17 18:00:27 +02:00
c57a0512ba Add assertions
Passing an unknown enum value to convert them to string would return
NULL without any error, possibly causing undefined behavior later.

Add assertions to catch such programming errors early.
2024-07-16 21:31:31 +02:00
e84db2914d Reorganize server packages
There are now a lot of classes in the server, reorganize them into
subpackages.
2024-07-11 22:38:00 +02:00
80ca7b15e5 Extract sources paths in build_without_gradle.sh
This avoids duplication, and will be useful to add more packages.
2024-07-11 22:34:58 +02:00
79242957a0 Add clipboard workaround for Honor device
Fixes #5073 <https://github.com/Genymobile/scrcpy/issues/5073>
2024-07-11 12:21:38 +02:00
fe7494c492 Linearize try-catch blocks
There are many possible method signatures for getPrimaryClip() and
setPrimaryClip().

Avoid the nested try-catch blocks.
2024-07-11 12:19:47 +02:00
9989668226 Add mouse secondary bindings
Add secondary bindings (Shift+click) for mouse buttons.

In addition to:

    --mouse-bind=xxxx

It is now possible to pass a sequence of secondary bindings:

    --mouse-bind=xxxx:xxxx
                 <--> <-->
             primary   secondary
            bindings   bindings

If the second sequence is omitted, then it is the same as the first one.

By default, for SDK mouse, primary bindings trigger shortcuts and
secondary bindings forward all clicks.

For AOA and UHID, the default bindings are reversed: all clicks are
forwarded by default, whereas pressing Shift+click trigger shortcuts.

    --mouse-bind=bhsn:++++  # default for SDK
    --mouse-bind=++++:bhsn  # default for AOA and UHID

Refs 035d60cf5d
Refs f5e6b8092a
Fixes #5055 <https://github.com/Genymobile/scrcpy/issues/5055>
PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
6baea57987 Track mouse buttons state manually
The buttons state was tracked by SDL_GetMouseState(), and scrcpy applied
a mask to ignore buttons used for shortcuts.

Instead, track the buttons actually pressed (ignoring shortcuts)
manually, to prepare the introduction of more dynamic mouse shortcuts.

PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
86b8286217 Remove unused virtual mouse
PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
51fee79bf5 Use finger source when a pointer is simulated
For pinch-to-zoom, rotation and tilt simulation, always use a finger
source (instead of a mouse) for both pointers (the real one and the
simulated one).

A "virtual" mouse does not work on all devices (e.g. on Pixel 8).

PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
6808288823 Make pointer id independent of mouse bindings
The device source (MOUSE or FINGER) to use depended on whether a
secondary click was possible via mouse bindings.

As a first step, always use a mouse source to break this dependency.
Note that this change might cause regressions in some (unknown) cases
(refs f70359f14f), but hopefully not.

Further commits will restore a finger source in some specific use cases,
but independent of secondary clicks.

Refs #5055 <https://github.com/Genymobile/scrcpy/issues/5055>
Fixes #5067 <https://github.com/Genymobile/scrcpy/issues/5067>
PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
0bce4d7f56 Add missing SC_ prefix for pointer id constants
PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
6d98766cd5 Simplify boolean condition using XOR
(A && !B) || (!A && B) <==> A ^ B

PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
487a6b9cf4 Remove top-level const
For consistency, never use top-level const for local variables.

PR #5076 <https://github.com/Genymobile/scrcpy/pull/5076>
2024-07-11 12:04:09 +02:00
b50f9eb41d Add workaround for Skyworth devices
The vendor-modified ROM of Skyworth devices needs a valid app
info/context.

Fixes #4922 <https://github.com/Genymobile/scrcpy/issues/4922>
2024-07-08 17:16:01 +02:00
Yan
46041e0cc0 Always initialize display->gl_context on macOS
Otherwise SDL_GL_DeleteContext() tried to access an uninitialized
pointer upon exit when not using the OpenGL renderer.

SDL_GL_DeleteContext() doesn't try to delete a NULL pointer, so no need
to check for that.

Fixes #5057 <https://github.com/Genymobile/scrcpy/issues/5057>
PR #5058 <https://github.com/Genymobile/scrcpy/pull/5058>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-06 00:11:48 +02:00
b419eef55e Do not report error on device disconnected
A device disconnection (when the adb connection is closed) makes the
read() on the "receiver" socket fail.

Since commit 063a8339ed, this is reported
as an error. As a consequence, scrcpy fails with:

    ERROR: Controller error

instead of:

    WARN: Device disconnected

To fix the issue, report a device disconnection in that case.

PR #5044 <https://github.com/Genymobile/scrcpy/pull/5044>
2024-07-06 00:04:07 +02:00
cc8e6133b0 Upgrade default versions in bug report template 2024-07-06 00:00:56 +02:00
126da0cb18 Rework bug report template checks
Remove explicit checkboxes, and add a link to prerequisites.
2024-07-06 00:00:55 +02:00
1d3b6dac69 Improve bug report template
Use titles and capital letters.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-05 23:57:53 +02:00
a4f8c02502 Reorder initialization to simplify
This also avoids a warning with some compilers which do not understand
that the condition to initialize the variable is the same as the
condition to use it:

    ../app/src/scrcpy.c: In function ‘scrcpy’:
    ../app/src/scrcpy.c:750:13: warning: ‘src’ may be used uninitialized in this function [-Wmaybe-uninitialized]
      750 |             sc_frame_source_add_sink(src, &s->screen.frame_sink);
          |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Refs 45fe6b602b
Refs <https://github.com/Genymobile/scrcpy/issues/5045#issuecomment-2201589757>
2024-07-02 08:14:58 +02:00
a8871bfad7 Update links to 2.5 2024-06-29 17:51:36 +02:00
89df38f641 Bump version to 2.5 2024-06-29 16:52:45 +02:00
c95e6964c5 Merge branch 'master' into release 2024-06-29 16:52:32 +02:00
343f715323 Upgrade platform-tools (35.0.0) for Windows 2024-06-29 13:10:45 +02:00
f13f00021f Upgrade SDL (2.30.4) for Windows 2024-06-29 13:10:45 +02:00
48c2c03093 Upgrade FFmpeg (7.0.1) for Windows 2024-06-29 13:10:45 +02:00
1e3deabd6c Do not call avcodec_close()
The documentation of avcodec_close() says:

> Do not use this function. Use avcodec_free_context() to destroy a
> codec context (either open or closed).

It was deprecated in FFmpeg 7 by commit
1cc24d749569a42510399a29b034f7a77bdec34e:

<1cc24d7495>

> Its use has been discouraged since 2016, but now is no longer used in
> avformat, so there is no reason to keep it public.
2024-06-29 13:10:45 +02:00
7633228278 Forward mouse hover events
Also add an option --no-mouse-hover to get the old behavior.

Fixes #2743 <https://github.com/Genymobile/scrcpy/issues/2743>
Fixes #3070 <https://github.com/Genymobile/scrcpy/issues/3070>
PR #5039 <https://github.com/Genymobile/scrcpy/pull/5039>
2024-06-29 12:42:19 +02:00
f5e6b8092a Forward all clicks by default for UHID/AOA
By default, only the left click is forwarded to the device, and
secondary clicks trigger shortcuts (the behavior can be configured by
--mouse-bind=xxxx).

But when the mouse mode is relative (AOA and UHID modes), forward all
clicks by default. This makes more sense since the cursor is handled on
the device side, the user expects all mouse buttons to be forwarded.

Refs <https://github.com/Genymobile/scrcpy/issues/4727#issuecomment-2069869750>
PR #5022 <https://github.com/Genymobile/scrcpy/pull/5022>
2024-06-24 23:17:59 +02:00
035d60cf5d Add option to configure mouse bindings
Add a new option --mouse-bind=xxxx.

The argument must be exactly 4 characters, one for each secondary click:

    --mouse-bind=xxxx
                 ^^^^
                 ||||
                 ||| `- 5th click
                 || `-- 4th click
                 | `--- middle click
                  `---- right click

Each character must be one of the following:
 - `+`: forward the click to the device
 - `-`: ignore the click
 - `b`: trigger shortcut BACK (or turn screen on if off)
 - `h`: trigger shortcut HOME
 - `s`: trigger shortcut APP_SWITCH
 - `n`: trigger shortcut "expand notification panel"

This deprecates --forward-all-clicks (use --mouse-bind=++++ instead).

Refs <https://github.com/Genymobile/scrcpy/pull/2258#issuecomment-2182394460>
PR #5022 <https://github.com/Genymobile/scrcpy/pull/5022>
2024-06-24 23:17:23 +02:00
40493dff60 Fix "resize to fit" when all clicks are forwarded
To resize the window to fit the device screen, it is possible to
double-click in the "black bars".

This feature was mistakenly disabled when --forward-all-clicks was set.

Instead, disable it only if mouse relative mode is enabled (AOA or
UHID), because in that case the mouse cursor is on the device.
2024-06-24 23:00:33 +02:00
09ce0307fe Fix zsh completion script
An '=' was missing for some options with an argument.
2024-06-24 22:56:49 +02:00
9fa30ab1ae Fix error message parameter
Use the local argument value, not the global optarg variable (even if it
has the same value in practice, as it's passed as argument).
2024-06-24 22:55:24 +02:00
0b926922bc Ignore shortcut keycodes
Never inject keycodes used as shortcut modifiers.

Refs #4732 <https://github.com/Genymobile/scrcpy/issues/4732>
PR #4741 <https://github.com/Genymobile/scrcpy/pull/4741>
2024-06-23 19:15:56 +02:00
24bcc3fa2b Simplify shortcut modifiers
Restrict shortcut modifiers to be composed of only one item each.

Before, it was possible to select a list of multiple combinations of
modifier keys, like --shortcut-mod='lctrl+lalt,rctrl+rsuper', meaning
that shortcuts would be triggered either by LCtrl+LAlt+key or
RCtrl+RSuper+key.

This was overly generic, probably not used very much, and it prevents to
solve inconsistencies between UP and DOWN events of modifier keys sent
to the device.

Refs #4732 <https://github.com/Genymobile/scrcpy/issues/4732>
PR #4741 <https://github.com/Genymobile/scrcpy/pull/4741>
2024-06-23 19:15:45 +02:00
592ca0b59b Try newer display API first
The old createDisplay() API has been removed from Android. Try the newer
API first, since more and more devices will use that version.

PR #5008 <https://github.com/Genymobile/scrcpy/pull/5008>
2024-06-21 14:25:47 +02:00
30e42af2d4 Add missing virtual display release()
PR #5008 <https://github.com/Genymobile/scrcpy/pull/5008>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-06-21 14:20:33 +02:00
9030bd8be4 Upgrade AGP from 8.1.3 to 8.3.0 2024-06-21 12:12:13 +02:00
576e7552a2 Mention that the Debian package is obsolete
It cannot be updated until the android-framework-XX Debian package is
fixed.

Refs <https://tracker.debian.org/pkg/scrcpy>
2024-06-13 09:14:40 +02:00
24b9e0a970 Retrieve icon decoder directly
The call to av_find_best_stream() gives the decoder directly, this
avoids to retrieve it afterwards in a separate step.
2024-06-11 10:04:27 +02:00
9ea4446369 Release the audio lock early
The final write from the writer thread does not require a lock: it is
guaranteed that enough space is available since the reader thread never
writes.
2024-06-09 19:25:32 +02:00
5d1d5bdc16 Fix thread leak on Windows
Fixes #4973 <https://github.com/Genymobile/scrcpy/issues/4973>
2024-06-09 18:27:30 +02:00
fd9498e07c Avoid zero-length copies
Return early if there is nothing to read/write.
2024-05-30 15:56:37 +02:00
c27ab46efb Remove suggestion to install from winget
It does not work.

Refs #4027 <https://github.com/Genymobile/scrcpy/issues/4027>
Refs #4389 <https://github.com/Genymobile/scrcpy/issues/4389>
Refs #4956 <https://github.com/Genymobile/scrcpy/issues/4956>
2024-05-30 08:23:42 +02:00
b5849db32f Document missing package to build for Windows
To build ffmpeg, libz is necessary.

Refs #4955 <https://github.com/Genymobile/scrcpy/issues/4955>
2024-05-29 10:32:58 +02:00
09e8c20168 Rename streamScreen() to streamCapture()
The capture source may be either the screen or the camera.
2024-05-14 08:23:57 +02:00
da484b7ab9 Reject recording with control only
If video and audio are disabled, there is nothing to record.
2024-05-12 10:44:27 +02:00
063a8339ed Terminate on controller error
This is particularly important to react to server socket disconnection
since video and audio may be disabled.

PR #4868 <https://github.com/Genymobile/scrcpy/pull/4868>
2024-05-11 17:12:00 +02:00
b5c8de08e0 Update documentation for --no-window
PR #4868 <https://github.com/Genymobile/scrcpy/pull/4868>
2024-05-11 17:12:00 +02:00
45fe6b602b Add scrcpy window without video playback
Add the possibility to solely control the device without screen
mirroring:

    scrcpy --no-video --no-audio

This is different from OTG mode, which does not require USB debugging at
all. Here, the standard mode is used but with the possibility to disable
video playback.

By default, always open a window (even without video playback), and add
an option --no-window.

Fixes #4727 <https://github.com/Genymobile/scrcpy/issues/4727>
Fixes #4793 <https://github.com/Genymobile/scrcpy/issues/4793>
PR #4868 <https://github.com/Genymobile/scrcpy/pull/4868>
2024-05-11 17:06:16 +02:00
cca2c9ffb7 Disable FPS counter when no video playback
There is no frame rate to count.
2024-04-19 12:57:18 +02:00
206809a99a Fix typo in documentation 2024-04-02 18:01:21 +02:00
131 changed files with 1890 additions and 747 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
github: [rom1v]
liberapay: rom1v
custom: ["https://paypal.me/rom2v"]

View File

@ -7,17 +7,25 @@ assignees: ''
---
- [ ] I have read the [FAQ](https://github.com/Genymobile/scrcpy/blob/master/FAQ.md).
- [ ] I have searched in existing [issues](https://github.com/Genymobile/scrcpy/issues).
_Please read the [prerequisites] to run scrcpy._
**Environment**
- OS: [e.g. Debian, Windows, macOS...]
- scrcpy version: [e.g. 1.12.1]
- installation method: [e.g. manual build, apt, snap, brew, Windows release...]
- device model:
- Android version: [e.g. 10]
[prerequisites]: https://github.com/Genymobile/scrcpy#prerequisites
_Also read the [FAQ] and check if your [issue][issues] already exists._
[FAQ]: https://github.com/Genymobile/scrcpy/blob/master/FAQ.md
[issues]: https://github.com/Genymobile/scrcpy/issues
## Environment
- **OS:** [e.g. Debian, Windows, macOS...]
- **Scrcpy version:** [e.g. 2.5]
- **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...]
- **Device model:**
- **Android version:** [e.g. 14]
## Describe the bug
**Describe the bug**
A clear and concise description of what the bug is.
On errors, please provide the output of the console (and `adb logcat` if relevant).

8
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@ -0,0 +1,8 @@
---
name: Question
about: Ask a question about scrcpy
title: ''
labels: ''
assignees: ''
---

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v2.4)
# scrcpy (v2.5)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -148,11 +148,14 @@ documented in the following pages:
## Contact
If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue].
You can open an [issue] for bug reports, feature requests or general questions.
For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution
to your problem immediately.
[issue]: https://github.com/Genymobile/scrcpy/issues
For general questions or discussions, you can also use:
You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View File

@ -6,6 +6,7 @@ _scrcpy() {
--audio-buffer=
--audio-codec=
--audio-codec-options=
--audio-dup
--audio-encoder=
--audio-source=
--audio-output-buffer=
@ -25,7 +26,6 @@ _scrcpy() {
-e --select-tcpip
-f --fullscreen
--force-adb-forward
--forward-all-clicks
-h --help
-K
--keyboard=
@ -41,6 +41,7 @@ _scrcpy() {
-M
--max-fps=
--mouse=
--mouse-bind=
-n --no-control
-N --no-playback
--no-audio
@ -50,6 +51,7 @@ _scrcpy() {
--no-downsize-on-error
--no-key-repeat
--no-mipmaps
--no-mouse-hover
--no-power-on
--no-video
--no-video-playback
@ -110,7 +112,7 @@ _scrcpy() {
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
return
;;
--camera-facing)

View File

@ -13,8 +13,9 @@ arguments=(
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-dup=[Duplicate audio]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)'
'--audio-source=[Select the audio source]:source:(output mic playback)'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--camera-ar=[Select the camera size by its aspect ratio]'
@ -32,10 +33,9 @@ arguments=(
{-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]'
{-h,--help}'[Print the help]'
'-K[Use UHID keyboard (same as --keyboard=uhid)]'
'--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-camera-sizes[List the valid camera capture sizes]'
@ -46,7 +46,8 @@ arguments=(
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'-M[Use UHID mouse (same as --mouse=uhid)]'
'--max-fps=[Limit the frame rate of screen capture]'
'--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
'--mouse-bind=[Configure bindings of secondary clicks]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--no-audio[Disable audio forwarding]'
@ -56,6 +57,7 @@ arguments=(
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-mouse-hover[Do not forward mouse hover events]'
'--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=34.0.5
VERSION=35.0.0
FILENAME=platform-tools_r$VERSION-windows.zip
PROJECT_DIR=platform-tools-$VERSION
SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571
SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e
cd "$SOURCES_DIR"

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=6.1.1
VERSION=7.0.1
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968
SHA256SUM=bce9eeb0f17ef8982390b1f37711a61b4290dc8c2a0c1a37b5857e85bfb0e4ff
cd "$SOURCES_DIR"
@ -17,7 +17,6 @@ then
else
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/ffmpeg-6.1-fix-build.patch
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"

View File

@ -1,27 +0,0 @@
From 03c80197afb324da38c9b70254231e3fdcfa68fc Mon Sep 17 00:00:00 2001
From: Romain Vimont <rom@rom1v.com>
Date: Sun, 12 Nov 2023 17:58:50 +0100
Subject: [PATCH] Fix FFmpeg 6.1 build
Build failed on tag n6.1 With --enable-decoder=av1 but without
--enable-muxer=av1.
---
libavcodec/Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 580a8d6b54..aff19b670c 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -249,7 +249,7 @@ OBJS-$(CONFIG_ATRAC3PAL_DECODER) += atrac3plusdec.o atrac3plus.o \
OBJS-$(CONFIG_ATRAC9_DECODER) += atrac9dec.o
OBJS-$(CONFIG_AURA_DECODER) += cyuv.o
OBJS-$(CONFIG_AURA2_DECODER) += aura.o
-OBJS-$(CONFIG_AV1_DECODER) += av1dec.o
+OBJS-$(CONFIG_AV1_DECODER) += av1dec.o av1_parse.o
OBJS-$(CONFIG_AV1_CUVID_DECODER) += cuviddec.o
OBJS-$(CONFIG_AV1_MEDIACODEC_DECODER) += mediacodecdec.o
OBJS-$(CONFIG_AV1_MEDIACODEC_ENCODER) += mediacodecenc.o
--
2.42.0

View File

@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=2.28.5
VERSION=2.30.5
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023
SHA256SUM=be3ca88f8c362704627a0bc5406edb2cd6cc6ba463596d81ebb7c2f18763d3bf
cd "$SOURCES_DIR"

View File

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

View File

@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.B \-\-audio\-dup
Duplicate audio (capture and keep playing on the device).
This feature is only available with --audio-source=playback.
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
@ -57,7 +63,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Select the audio source (output, mic or playback).
The "output" source forwards the whole audio output, and disables playback on the device.
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
The "mic" source captures the microphone.
Default is output.
@ -163,10 +175,6 @@ Start in fullscreen.
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
.TP
.B \-\-forward\-all\-clicks
By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead.
.TP
.B \-h, \-\-help
Print this help.
@ -261,6 +269,27 @@ LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse bac
Also see \fB\-\-keyboard\fR.
.TP
.BI "\-\-mouse\-bind " xxxx[:xxxx]
Configure bindings of secondary clicks.
The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click).
The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held.
If the second sequence of bindings is omitted, then it is the same as the first one.
Each character must be one of the following:
- '+': forward the click to the device
- '-': ignore the click
- 'b': trigger shortcut BACK (or turn screen on if off)
- 'h': trigger shortcut HOME
- 's': trigger shortcut APP_SWITCH
- 'n': trigger shortcut "expand notification panel"
Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID.
.TP
.B \-n, \-\-no\-control
@ -304,6 +333,10 @@ Do not forward repeated key events when a key is held down.
.B \-\-no\-mipmaps
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
.TP
.B \-\-no\-mouse\-hover
Do not forward mouse hover (mouse motion without any clicks) events.
.TP
.B \-\-no\-power\-on
Do not power on the device on start.
@ -316,6 +349,10 @@ Disable video forwarding.
.B \-\-no\-video\-playback
Disable video playback on the computer.
.TP
.B \-\-no\-window
Disable scrcpy window. Implies --no-video-playback and --no-control.
.TP
.BI "\-\-orientation " value
Same as --display-orientation=value --record-orientation=value.
@ -420,9 +457,9 @@ Turn the device screen off immediately.
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
Several shortcut modifiers can be specified, separated by ','.
For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper".
For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper".
Default is "lalt,lsuper" (left-Alt or left-Super).

View File

@ -194,7 +194,11 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
SDL_UnlockAudioDevice(ap->device);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
@ -202,8 +206,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
assert(w == remaining);
(void) w;
}
SDL_UnlockAudioDevice(ap->device);
}
uint32_t underflow = 0;

View File

@ -97,6 +97,10 @@ enum {
OPT_MOUSE,
OPT_HID_KEYBOARD_DEPRECATED,
OPT_HID_MOUSE_DEPRECATED,
OPT_NO_WINDOW,
OPT_MOUSE_BIND,
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
};
struct sc_option {
@ -174,6 +178,13 @@ static const struct sc_option options[] = {
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_DUP,
.longopt = "audio-dup",
.text = "Duplicate audio (capture and keep playing on the device).\n"
"This feature is only available with --audio-source=playback."
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
@ -186,7 +197,13 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
.text = "Select the audio source (output, mic or playback).\n"
"The \"output\" source forwards the whole audio output, and "
"disables playback on the device.\n"
"The \"playback\" source captures the audio playback (Android "
"apps can opt-out, so the whole output is not necessarily "
"captured).\n"
"The \"mic\" source captures the microphone.\n"
"Default is output.",
},
{
@ -351,11 +368,9 @@ static const struct sc_option options[] = {
"device.",
},
{
// deprecated
.longopt_id = OPT_FORWARD_ALL_CLICKS,
.longopt = "forward-all-clicks",
.text = "By default, right-click triggers BACK (or POWER on) and "
"middle-click triggers HOME. This option disables these "
"shortcuts and forwards the clicks to the device instead.",
},
{
.shortopt = 'h',
@ -489,6 +504,30 @@ static const struct sc_option options[] = {
"control of the mouse back to the computer.\n"
"Also see --keyboard.",
},
{
.longopt_id = OPT_MOUSE_BIND,
.longopt = "mouse-bind",
.argdesc = "xxxx[:xxxx]",
.text = "Configure bindings of secondary clicks.\n"
"The argument must be one or two sequences (separated by ':') "
"of exactly 4 characters, one for each secondary click (in "
"order: right click, middle click, 4th click, 5th click).\n"
"The first sequence defines the primary bindings, used when a "
"mouse button is pressed alone. The second sequence defines "
"the secondary bindings, used when a mouse button is pressed "
"while the Shift key is held.\n"
"If the second sequence of bindings is omitted, then it is the "
"same as the first one.\n"
"Each character must be one of the following:\n"
" '+': forward the click to the device\n"
" '-': ignore the click\n"
" 'b': trigger shortcut BACK (or turn screen on if off)\n"
" 'h': trigger shortcut HOME\n"
" 's': trigger shortcut APP_SWITCH\n"
" 'n': trigger shortcut \"expand notification panel\"\n"
"Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA "
"and UHID.",
},
{
.shortopt = 'n',
.longopt = "no-control",
@ -551,6 +590,12 @@ static const struct sc_option options[] = {
"mipmaps are automatically generated to improve downscaling "
"quality. This option disables the generation of mipmaps.",
},
{
.longopt_id = OPT_NO_MOUSE_HOVER,
.longopt = "no-mouse-hover",
.text = "Do not forward mouse hover (mouse motion without any clicks) "
"events.",
},
{
.longopt_id = OPT_NO_POWER_ON,
.longopt = "no-power-on",
@ -566,6 +611,12 @@ static const struct sc_option options[] = {
.longopt = "no-video-playback",
.text = "Disable video playback on the computer.",
},
{
.longopt_id = OPT_NO_WINDOW,
.longopt = "no-window",
.text = "Disable scrcpy window. Implies --no-video-playback and "
"--no-control.",
},
{
.longopt_id = OPT_ORIENTATION,
.longopt = "orientation",
@ -709,10 +760,10 @@ static const struct sc_option options[] = {
.text = "Specify the modifiers to use for scrcpy shortcuts.\n"
"Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", "
"\"lsuper\" and \"rsuper\".\n"
"A shortcut can consist in several keys, separated by '+'. "
"Several shortcuts can be specified, separated by ','.\n"
"For example, to use either LCtrl+LAlt or LSuper for scrcpy "
"shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"Several shortcut modifiers can be specified, separated by "
"','.\n"
"For example, to use either LCtrl or LSuper for scrcpy "
"shortcuts, pass \"lctrl,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
@ -1680,82 +1731,62 @@ parse_log_level(const char *s, enum sc_log_level *log_level) {
return false;
}
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error)
static unsigned
static enum sc_shortcut_mod
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RSUPER;
} else {
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) key_len, item);
return 0;
}
if (STREQ("lctrl", item, len)) {
return SC_SHORTCUT_MOD_LCTRL;
}
if (STREQ("rctrl", item, len)) {
return SC_SHORTCUT_MOD_RCTRL;
}
if (STREQ("lalt", item, len)) {
return SC_SHORTCUT_MOD_LALT;
}
if (STREQ("ralt", item, len)) {
return SC_SHORTCUT_MOD_RALT;
}
if (STREQ("lsuper", item, len)) {
return SC_SHORTCUT_MOD_LSUPER;
}
if (STREQ("rsuper", item, len)) {
return SC_SHORTCUT_MOD_RSUPER;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
bool has_plus = strchr(item, '+');
if (has_plus) {
LOGE("Shortcut mod combination with '+' is not supported anymore: "
"'%.*s' (see #4741)", (int) len, item);
return 0;
}
return mod;
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) len, item);
return 0;
}
static bool
parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
parse_shortcut_mods(const char *s, uint8_t *shortcut_mods) {
uint8_t mods = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
// A list of shortcut modifiers, for example "lctrl,rctrl,rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
enum sc_shortcut_mod mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
mods |= mod;
if (!comma) {
break;
@ -1764,7 +1795,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
s = comma + 1;
}
mods->count = count;
*shortcut_mods = mods;
return true;
}
@ -1772,7 +1803,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
sc_parse_shortcut_mods(const char *s, uint8_t *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
@ -1914,7 +1945,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
if (!strcmp(optarg, "playback")) {
*source = SC_AUDIO_SOURCE_PLAYBACK;
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
optarg);
return false;
}
@ -2051,11 +2088,85 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
}
LOGE("Unsupported pause on exit mode: %s "
"(expected true, false or if-error)", optarg);
"(expected true, false or if-error)", s);
return false;
}
static bool
parse_mouse_binding(char c, enum sc_mouse_binding *b) {
switch (c) {
case '+':
*b = SC_MOUSE_BINDING_CLICK;
return true;
case '-':
*b = SC_MOUSE_BINDING_DISABLED;
return true;
case 'b':
*b = SC_MOUSE_BINDING_BACK;
return true;
case 'h':
*b = SC_MOUSE_BINDING_HOME;
return true;
case 's':
*b = SC_MOUSE_BINDING_APP_SWITCH;
return true;
case 'n':
*b = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL;
return true;
default:
LOGE("Invalid mouse binding: '%c' "
"(expected '+', '-', 'b', 'h', 's' or 'n')", c);
return false;
}
}
static bool
parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) {
assert(strlen(s) >= 4);
if (!parse_mouse_binding(s[0], &mbs->right_click)) {
return false;
}
if (!parse_mouse_binding(s[1], &mbs->middle_click)) {
return false;
}
if (!parse_mouse_binding(s[2], &mbs->click4)) {
return false;
}
if (!parse_mouse_binding(s[3], &mbs->click5)) {
return false;
}
return true;
}
static bool
parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) {
size_t len = strlen(s);
// either "xxxx" or "xxxx:xxxx"
if (len != 4 && (len != 9 || s[4] != ':')) {
LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', "
"with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s);
return false;
}
if (!parse_mouse_binding_set(s, &mb->pri)) {
return false;
}
if (len == 9) {
if (!parse_mouse_binding_set(s + 5, &mb->sec)) {
return false;
}
} else {
// use the same bindings for Shift+click
mb->sec = mb->pri;
}
return true;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@ -2138,6 +2249,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_MOUSE_BIND:
if (!parse_mouse_bindings(optarg, &opts->mouse_bindings)) {
return false;
}
break;
case OPT_NO_MOUSE_HOVER:
opts->mouse_hover = false;
break;
case OPT_HID_MOUSE_DEPRECATED:
LOGE("--hid-mouse has been removed, use --mouse=aoa or "
"--mouse=uhid instead.");
@ -2335,7 +2454,22 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case OPT_FORWARD_ALL_CLICKS:
opts->forward_all_clicks = true;
LOGW("--forward-all-clicks is deprecated, "
"use --mouse-bind=++++ instead.");
opts->mouse_bindings = (struct sc_mouse_bindings) {
.pri = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
.sec = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
};
break;
case OPT_LEGACY_PASTE:
opts->legacy_paste = true;
@ -2486,6 +2620,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_CAMERA_HIGH_SPEED:
opts->camera_high_speed = true;
break;
case OPT_NO_WINDOW:
opts->window = false;
break;
case OPT_AUDIO_DUP:
opts->audio_dup = true;
break;
default:
// getopt prints the error message on stderr
return false;
@ -2523,6 +2663,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
v4l2 = !!opts->v4l2_device;
#endif
if (!opts->window) {
// Without window, there cannot be any video playback or control
opts->video_playback = false;
opts->control = false;
}
if (!opts->video) {
opts->video_playback = false;
// Do not power on the device on start if video capture is disabled
@ -2544,8 +2690,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->audio = false;
}
if (!opts->video && !opts->audio && !otg) {
LOGE("No video, no audio, no OTG: nothing to do");
if (!opts->video && !opts->audio && !opts->control && !otg) {
LOGE("No video, no audio, no control, no OTG: nothing to do");
return false;
}
@ -2569,6 +2715,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
#ifdef HAVE_V4L2
if (v4l2) {
if (!opts->video) {
LOGE("V4L2 sink requires video capture, but --no-video was set.");
return false;
}
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
@ -2588,13 +2739,59 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_SDK;
if (opts->control) {
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_SDK;
}
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
} else if (!opts->video_playback) {
LOGI("No video mirroring, mouse mode switched to UHID");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
} else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
}
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK
&& !opts->video_playback) {
LOGE("SDK mouse mode requires video playback. Try --mouse=uhid.");
return false;
}
}
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
: SC_MOUSE_INPUT_MODE_SDK;
// If mouse bindings are not explictly set, configure default bindings
if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) {
assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO);
static struct sc_mouse_binding_set default_shortcuts = {
.right_click = SC_MOUSE_BINDING_BACK,
.middle_click = SC_MOUSE_BINDING_HOME,
.click4 = SC_MOUSE_BINDING_APP_SWITCH,
.click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
static struct sc_mouse_binding_set forward = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
};
// By default, forward all clicks only for UHID and AOA
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
opts->mouse_bindings.pri = default_shortcuts;
opts->mouse_bindings.sec = forward;
} else {
opts->mouse_bindings.pri = forward;
opts->mouse_bindings.sec = default_shortcuts;
}
}
if (otg) {
@ -2641,6 +2838,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->mouse_input_mode != SC_MOUSE_INPUT_MODE_SDK
&& !opts->mouse_hover) {
LOGE("--no-mouse-over is specific to --mouse=sdk");
return false;
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
@ -2692,19 +2895,42 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
if (opts->audio_dup) {
LOGI("Audio duplication enabled: audio source switched to "
"\"playback\"");
opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK;
} else {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
}
} else {
opts->audio_source = SC_AUDIO_SOURCE_MIC;
LOGI("Camera video source: microphone audio source selected");
}
}
if (opts->audio_dup) {
if (!opts->audio) {
LOGE("--audio-dup not supported if audio is disabled");
return false;
}
if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) {
LOGE("--audio-dup is specific to --audio-source=playback");
return false;
}
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (opts->record_filename) {
if (!opts->video && !opts->audio) {
LOGE("Video and audio disabled, nothing to record");
return false;
}
if (!opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
@ -2811,6 +3037,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
# endif
if (opts->start_fps_counter && !opts->video_playback) {
LOGW("--print-fps has no effect without video playback");
opts->start_fps_counter = false;
}
if (otg) {
// OTG mode is compatible with only very few options.
// Only report obvious errors.

View File

@ -28,7 +28,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]);
#ifdef SC_TEST
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods);
sc_parse_shortcut_mods(const char *s, uint8_t *shortcut_mods);
#endif
#endif

View File

@ -64,13 +64,11 @@ static const char *const copy_key_labels[] = {
static inline const char *
get_well_known_pointer_id_name(uint64_t pointer_id) {
switch (pointer_id) {
case POINTER_ID_MOUSE:
case SC_POINTER_ID_MOUSE:
return "mouse";
case POINTER_ID_GENERIC_FINGER:
case SC_POINTER_ID_GENERIC_FINGER:
return "finger";
case POINTER_ID_VIRTUAL_MOUSE:
return "vmouse";
case POINTER_ID_VIRTUAL_FINGER:
case SC_POINTER_ID_VIRTUAL_FINGER:
return "vfinger";
default:
return NULL;

View File

@ -18,12 +18,11 @@
// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
#define POINTER_ID_MOUSE UINT64_C(-1)
#define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
#define SC_POINTER_ID_MOUSE UINT64_C(-1)
#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2)
// Used for injecting an additional virtual pointer for pinch-to-zoom
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3)
enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,

View File

@ -6,8 +6,20 @@
#define SC_CONTROL_MSG_QUEUE_MAX 64
static void
sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error,
void *userdata) {
(void) receiver;
struct sc_controller *controller = userdata;
// Forward the event to the controller listener
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
}
bool
sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
const struct sc_controller_callbacks *cbs,
void *cbs_userdata) {
sc_vecdeque_init(&controller->queue);
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
@ -15,7 +27,12 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
return false;
}
ok = sc_receiver_init(&controller->receiver, control_socket);
static const struct sc_receiver_callbacks receiver_cbs = {
.on_ended = sc_controller_receiver_on_ended,
};
ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs,
controller);
if (!ok) {
sc_vecdeque_destroy(&controller->queue);
return false;
@ -39,6 +56,10 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
controller->control_socket = control_socket;
controller->stopped = false;
assert(cbs && cbs->on_ended);
controller->cbs = cbs;
controller->cbs_userdata = cbs_userdata;
return true;
}
@ -90,21 +111,30 @@ sc_controller_push_msg(struct sc_controller *controller,
static bool
process_msg(struct sc_controller *controller,
const struct sc_control_msg *msg) {
const struct sc_control_msg *msg, bool *eos) {
static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
size_t length = sc_control_msg_serialize(msg, serialized_msg);
if (!length) {
*eos = false;
return false;
}
ssize_t w =
net_send_all(controller->control_socket, serialized_msg, length);
return (size_t) w == length;
if ((size_t) w != length) {
*eos = true;
return false;
}
return true;
}
static int
run_controller(void *data) {
struct sc_controller *controller = data;
bool error = false;
for (;;) {
sc_mutex_lock(&controller->mutex);
while (!controller->stopped
@ -114,6 +144,7 @@ run_controller(void *data) {
if (controller->stopped) {
// stop immediately, do not process further msgs
sc_mutex_unlock(&controller->mutex);
LOGD("Controller stopped");
break;
}
@ -121,13 +152,20 @@ run_controller(void *data) {
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
sc_mutex_unlock(&controller->mutex);
bool ok = process_msg(controller, &msg);
bool eos;
bool ok = process_msg(controller, &msg, &eos);
sc_control_msg_destroy(&msg);
if (!ok) {
LOGD("Could not write msg to socket");
if (eos) {
LOGD("Controller stopped (socket closed)");
} // else error already logged
error = !eos;
break;
}
}
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
return 0;
}

View File

@ -22,10 +22,20 @@ struct sc_controller {
bool stopped;
struct sc_control_msg_queue queue;
struct sc_receiver receiver;
const struct sc_controller_callbacks *cbs;
void *cbs_userdata;
};
struct sc_controller_callbacks {
void (*on_ended)(struct sc_controller *controller, bool error,
void *userdata);
};
bool
sc_controller_init(struct sc_controller *controller, sc_socket control_socket);
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
const struct sc_controller_callbacks *cbs,
void *cbs_userdata);
void
sc_controller_configure(struct sc_controller *controller,

View File

@ -278,7 +278,6 @@ run_demuxer(void *data) {
finally_close_sinks:
sc_packet_source_sinks_close(&demuxer->packet_source);
finally_free_context:
// This also calls avcodec_close() internally
avcodec_free_context(&codec_ctx);
end:
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);

View File

@ -5,8 +5,30 @@
#include "util/log.h"
static bool
sc_display_init_novideo_icon(struct sc_display *display,
SDL_Surface *icon_novideo) {
assert(icon_novideo);
if (SDL_RenderSetLogicalSize(display->renderer,
icon_novideo->w, icon_novideo->h)) {
LOGW("Could not set renderer logical size: %s", SDL_GetError());
// don't fail
}
display->texture = SDL_CreateTextureFromSurface(display->renderer,
icon_novideo);
if (!display->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
return true;
}
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps) {
display->renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!display->renderer) {
@ -21,6 +43,10 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
display->mipmaps = false;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
display->gl_context = NULL;
#endif
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
@ -68,6 +94,18 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
display->pending.frame = NULL;
display->has_frame = false;
if (icon_novideo) {
// Without video, set a static scrcpy icon as window content
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
if (!ok) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
#endif
SDL_DestroyRenderer(display->renderer);
return false;
}
}
return true;
}

View File

@ -44,7 +44,8 @@ enum sc_display_result {
};
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);

View File

@ -7,3 +7,4 @@
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)
#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9)

View File

@ -78,7 +78,19 @@ decode_image(const char *path) {
goto close_input;
}
int stream = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
// In ffmpeg/doc/APIchanges:
// 2021-04-27 - 46dac8cf3d - lavf 59.0.100 - avformat.h
// av_find_best_stream now uses a const AVCodec ** parameter
// for the returned decoder.
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(59, 0, 100)
const AVCodec *codec;
#else
AVCodec *codec;
#endif
int stream =
av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &codec, 0);
if (stream < 0 ) {
LOGE("Could not find best image stream");
goto close_input;
@ -86,12 +98,6 @@ decode_image(const char *path) {
AVCodecParameters *params = ctx->streams[stream]->codecpar;
const AVCodec *codec = avcodec_find_decoder(params->codec_id);
if (!codec) {
LOGE("Could not find image decoder");
goto close_input;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOG_OOM();
@ -111,21 +117,21 @@ decode_image(const char *path) {
AVFrame *frame = av_frame_alloc();
if (!frame) {
LOG_OOM();
goto close_codec;
goto free_codec_ctx;
}
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOG_OOM();
av_frame_free(&frame);
goto close_codec;
goto free_codec_ctx;
}
if (av_read_frame(ctx, packet) < 0) {
LOGE("Could not read frame");
av_packet_free(&packet);
av_frame_free(&frame);
goto close_codec;
goto free_codec_ctx;
}
int ret;
@ -133,22 +139,20 @@ decode_image(const char *path) {
LOGE("Could not send icon packet: %d", ret);
av_packet_free(&packet);
av_frame_free(&frame);
goto close_codec;
goto free_codec_ctx;
}
if ((ret = avcodec_receive_frame(codec_ctx, frame)) != 0) {
LOGE("Could not receive icon frame: %d", ret);
av_packet_free(&packet);
av_frame_free(&frame);
goto close_codec;
goto free_codec_ctx;
}
av_packet_free(&packet);
result = frame;
close_codec:
avcodec_close(codec_ctx);
free_codec_ctx:
avcodec_free_context(&codec_ctx);
close_input:

View File

@ -9,6 +9,7 @@
#include <SDL2/SDL_events.h>
#include "coords.h"
#include "options.h"
/* The representation of input events in scrcpy is very close to the SDL API,
* for simplicity.
@ -436,19 +437,11 @@ sc_mouse_button_from_sdl(uint8_t button) {
}
static inline uint8_t
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state,
bool forward_all_clicks) {
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
assert(buttons_state < 0x100); // fits in uint8_t
uint8_t mask = SC_MOUSE_BUTTON_LEFT;
if (forward_all_clicks) {
mask |= SC_MOUSE_BUTTON_RIGHT
| SC_MOUSE_BUTTON_MIDDLE
| SC_MOUSE_BUTTON_X1
| SC_MOUSE_BUTTON_X2;
}
return buttons_state & mask;
// SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index)
return buttons_state;
}
#endif

View File

@ -10,7 +10,7 @@
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(unsigned shortcut_mod) {
to_sdl_mod(uint8_t shortcut_mod) {
uint16_t sdl_mod = 0;
if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
@ -38,15 +38,18 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
assert(im->sdl_shortcut_mods.count);
assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) {
if (im->sdl_shortcut_mods.data[i] == sdl_mod) {
return true;
}
}
// at least one shortcut mod pressed?
return sdl_mod & im->sdl_shortcut_mods;
}
return false;
static bool
is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
void
@ -64,24 +67,18 @@ sc_input_manager_init(struct sc_input_manager *im,
im->kp = params->kp;
im->mp = params->mp;
im->forward_all_clicks = params->forward_all_clicks;
im->mouse_bindings = params->mouse_bindings;
im->legacy_paste = params->legacy_paste;
im->clipboard_autosync = params->clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods;
assert(shortcut_mods->count);
assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < shortcut_mods->count; ++i) {
uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]);
assert(sdl_mod);
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods);
im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->mouse_buttons_state = 0;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
@ -370,9 +367,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id =
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0;
@ -403,6 +398,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
// controller is NULL if --no-control is requested
bool control = im->controller;
bool paused = im->screen->paused;
bool video = im->screen->video;
SDL_Keycode keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
@ -411,7 +407,12 @@ sc_input_manager_process_key(struct sc_input_manager *im,
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
bool smod = is_shortcut_mod(im, mod);
// Either the modifier includes a shortcut modifier, or the key
// press/release is a modifier key.
// The second condition is necessary to ignore the release of the modifier
// key (because in this case mod is 0).
bool is_shortcut = is_shortcut_mod(im, mod)
|| is_shortcut_key(im, keycode);
if (down && !repeat) {
if (keycode == im->last_keycode && mod == im->last_mod) {
@ -423,8 +424,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
// The shortcut modifier is pressed
if (smod) {
if (is_shortcut) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
switch (keycode) {
case SDLK_h:
@ -462,13 +462,13 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_z:
if (down && !repeat) {
if (video && down && !repeat) {
sc_screen_set_paused(im->screen, !shift);
}
return;
case SDLK_DOWN:
if (shift) {
if (!repeat && down) {
if (video && !repeat && down) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180);
}
@ -479,7 +479,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return;
case SDLK_UP:
if (shift) {
if (!repeat && down) {
if (video && !repeat && down) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180);
}
@ -489,7 +489,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_LEFT:
if (!repeat && down) {
if (video && !repeat && down) {
if (shift) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0);
@ -500,7 +500,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_RIGHT:
if (!repeat && down) {
if (video && !repeat && down) {
if (shift) {
apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0);
@ -533,22 +533,22 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
return;
case SDLK_f:
if (!shift && !repeat && down) {
if (video && !shift && !repeat && down) {
sc_screen_switch_fullscreen(im->screen);
}
return;
case SDLK_w:
if (!shift && !repeat && down) {
if (video && !shift && !repeat && down) {
sc_screen_resize_to_fit(im->screen);
}
return;
case SDLK_g:
if (!shift && !repeat && down) {
if (video && !shift && !repeat && down) {
sc_screen_resize_to_pixel_perfect(im->screen);
}
return;
case SDLK_i:
if (!shift && !repeat && down) {
if (video && !shift && !repeat && down) {
switch_fps_counter_state(im);
}
return;
@ -625,6 +625,23 @@ sc_input_manager_process_key(struct sc_input_manager *im,
im->kp->ops->process_key(im->kp, &evt, ack_to_wait);
}
static struct sc_position
sc_input_manager_get_position(struct sc_input_manager *im, int32_t x,
int32_t y) {
if (im->mp->relative_mode) {
// No absolute position
return (struct sc_position) {
.screen_size = {0, 0},
.point = {0, 0},
};
}
return (struct sc_position) {
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen, x, y),
};
}
static void
sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
const SDL_MouseMotionEvent *event) {
@ -634,19 +651,12 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
}
struct sc_mouse_motion_event evt = {
.position = {
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
event->x,
event->y),
},
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.position = sc_input_manager_get_position(im, event->x, event->y),
.pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state =
sc_mouse_buttons_state_from_sdl(event->state,
im->forward_all_clicks),
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_motion);
@ -697,6 +707,25 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
im->mp->ops->process_touch(im->mp, &evt);
}
static enum sc_mouse_binding
sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
uint8_t sdl_button) {
switch (sdl_button) {
case SDL_BUTTON_LEFT:
return SC_MOUSE_BINDING_CLICK;
case SDL_BUTTON_RIGHT:
return bindings->right_click;
case SDL_BUTTON_MIDDLE:
return bindings->middle_click;
case SDL_BUTTON_X1:
return bindings->click4;
case SDL_BUTTON_X2:
return bindings->click5;
default:
return SC_MOUSE_BINDING_DISABLED;
}
}
static void
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
const SDL_MouseButtonEvent *event) {
@ -708,70 +737,100 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
bool down = event->type == SDL_MOUSEBUTTONDOWN;
if (!im->forward_all_clicks) {
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
if (im->kp && event->button == SDL_BUTTON_X1) {
action_app_switch(im, action);
enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button);
if (!down) {
// Mark the button as released
im->mouse_buttons_state &= ~button;
}
SDL_Keymod keymod = SDL_GetModState();
bool ctrl_pressed = keymod & KMOD_CTRL;
bool shift_pressed = keymod & KMOD_SHIFT;
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
struct sc_mouse_binding_set *bindings = !shift_pressed
? &im->mouse_bindings.pri
: &im->mouse_bindings.sec;
enum sc_mouse_binding binding =
sc_input_manager_get_binding(bindings, event->button);
assert(binding != SC_MOUSE_BINDING_AUTO);
switch (binding) {
case SC_MOUSE_BINDING_DISABLED:
// ignore click
return;
}
if (event->button == SDL_BUTTON_X2 && down) {
if (event->clicks < 2) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
case SC_MOUSE_BINDING_BACK:
if (im->kp) {
press_back_or_turn_screen_on(im, action);
}
return;
}
if (im->kp && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im, action);
case SC_MOUSE_BINDING_HOME:
if (im->kp) {
action_home(im, action);
}
return;
}
if (im->kp && event->button == SDL_BUTTON_MIDDLE) {
action_home(im, action);
case SC_MOUSE_BINDING_APP_SWITCH:
if (im->kp) {
action_app_switch(im, action);
}
return;
}
}
// double-click on black borders resize to fit the device screen
if (event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
case SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL:
if (down) {
sc_screen_resize_to_fit(im->screen);
if (event->clicks < 2) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
}
}
return;
}
default:
assert(binding == SC_MOUSE_BINDING_CLICK);
break;
}
}
// double-click on black borders resizes to fit the device screen
bool video = im->screen->video;
bool mouse_relative_mode = im->mp && im->mp->relative_mode;
if (video && !mouse_relative_mode && event->button == SDL_BUTTON_LEFT
&& event->clicks == 2) {
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
if (down) {
sc_screen_resize_to_fit(im->screen);
}
return;
}
// otherwise, send the click event to the device
}
if (!im->mp || paused) {
return;
}
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
if (down) {
// Mark the button as pressed
im->mouse_buttons_state |= button;
}
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
(!down && im->vfinger_down));
bool use_finger = im->vfinger_down || change_vfinger;
struct sc_mouse_click_event evt = {
.position = {
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
event->x,
event->y),
},
.position = sc_input_manager_get_position(im, event->x, event->y),
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
im->forward_all_clicks),
.pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_click);
@ -797,14 +856,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
const SDL_Keymod keymod = SDL_GetModState();
const bool ctrl_pressed = keymod & KMOD_CTRL;
const bool shift_pressed = keymod & KMOD_SHIFT;
if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) {
if (change_vfinger) {
struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y);
@ -837,13 +889,10 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
int mouse_x;
int mouse_y;
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
(void) buttons; // Actual buttons are tracked manually to ignore shortcuts
struct sc_mouse_scroll_event evt = {
.position = {
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
mouse_x, mouse_y),
},
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
#if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
@ -851,8 +900,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.buttons_state =
sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks),
.buttons_state = im->mouse_buttons_state,
};
im->mp->ops->process_mouse_scroll(im->mp, &evt);

View File

@ -22,19 +22,18 @@ struct sc_input_manager {
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
struct {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
} sdl_shortcut_mods;
uint16_t sdl_shortcut_mods;
bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values
// Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of
// system-generated repeated key presses.
@ -52,10 +51,10 @@ struct sc_input_manager_params {
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
void

View File

@ -58,17 +58,18 @@ convert_touch_action(enum sc_touch_action action) {
static void
sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
const struct sc_mouse_motion_event *event) {
if (!event->buttons_state) {
struct sc_mouse_sdk *m = DOWNCAST(mp);
if (!m->mouse_hover && !event->buttons_state) {
// Do not send motion events when no click is pressed
return;
}
struct sc_mouse_sdk *m = DOWNCAST(mp);
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = AMOTION_EVENT_ACTION_MOVE,
.action = event->buttons_state ? AMOTION_EVENT_ACTION_MOVE
: AMOTION_EVENT_ACTION_HOVER_MOVE,
.pointer_id = event->pointer_id,
.position = event->position,
.pressure = 1.f,
@ -145,8 +146,10 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp,
}
void
sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) {
sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller,
bool mouse_hover) {
m->controller = controller;
m->mouse_hover = mouse_hover;
static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion,

View File

@ -13,9 +13,11 @@ struct sc_mouse_sdk {
struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_controller *controller;
bool mouse_hover;
};
void
sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller);
sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller,
bool mouse_hover);
#endif

View File

@ -23,6 +23,20 @@ const struct scrcpy_options scrcpy_options_default = {
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO,
.mouse_bindings = {
.pri = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
.sec = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
},
.camera_facing = SC_CAMERA_FACING_ANY,
.port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
@ -30,10 +44,7 @@ const struct scrcpy_options scrcpy_options_default = {
},
.tunnel_host = 0,
.tunnel_port = 0,
.shortcut_mods = {
.data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER},
.count = 2,
},
.shortcut_mods = SC_SHORTCUT_MOD_LALT | SC_SHORTCUT_MOD_LSUPER,
.max_size = 0,
.video_bit_rate = 0,
.audio_bit_rate = 0,
@ -71,7 +82,6 @@ const struct scrcpy_options scrcpy_options_default = {
.force_adb_forward = false,
.disable_screensaver = false,
.forward_key_repeat = true,
.forward_all_clicks = false,
.legacy_paste = false,
.power_off_on_close = false,
.clipboard_autosync = true,
@ -89,6 +99,9 @@ const struct scrcpy_options scrcpy_options_default = {
.kill_adb_on_close = false,
.camera_high_speed = false,
.list = 0,
.window = true,
.mouse_hover = true,
.audio_dup = false,
};
enum sc_orientation

View File

@ -59,6 +59,7 @@ enum sc_audio_source {
SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
SC_AUDIO_SOURCE_PLAYBACK,
};
enum sc_camera_facing {
@ -155,6 +156,28 @@ enum sc_mouse_input_mode {
SC_MOUSE_INPUT_MODE_AOA,
};
enum sc_mouse_binding {
SC_MOUSE_BINDING_AUTO,
SC_MOUSE_BINDING_DISABLED,
SC_MOUSE_BINDING_CLICK,
SC_MOUSE_BINDING_BACK,
SC_MOUSE_BINDING_HOME,
SC_MOUSE_BINDING_APP_SWITCH,
SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
struct sc_mouse_binding_set {
enum sc_mouse_binding right_click;
enum sc_mouse_binding middle_click;
enum sc_mouse_binding click4;
enum sc_mouse_binding click5;
};
struct sc_mouse_bindings {
struct sc_mouse_binding_set pri;
struct sc_mouse_binding_set sec; // When Shift is pressed
};
enum sc_key_inject_mode {
// Inject special keys, letters and space as key events.
// Inject numbers and punctuation as text events.
@ -169,8 +192,6 @@ enum sc_key_inject_mode {
SC_KEY_INJECT_MODE_RAW,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
SC_SHORTCUT_MOD_LCTRL = 1 << 0,
SC_SHORTCUT_MOD_RCTRL = 1 << 1,
@ -180,11 +201,6 @@ enum sc_shortcut_mod {
SC_SHORTCUT_MOD_RSUPER = 1 << 5,
};
struct sc_shortcut_mods {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
};
struct sc_port_range {
uint16_t first;
uint16_t last;
@ -215,11 +231,12 @@ struct scrcpy_options {
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;
struct sc_mouse_bindings mouse_bindings;
enum sc_camera_facing camera_facing;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
struct sc_shortcut_mods shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
uint16_t max_size;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
@ -257,7 +274,6 @@ struct scrcpy_options {
bool force_adb_forward;
bool disable_screensaver;
bool forward_key_repeat;
bool forward_all_clicks;
bool legacy_paste;
bool power_off_on_close;
bool clipboard_autosync;
@ -279,6 +295,9 @@ struct scrcpy_options {
#define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8
uint8_t list;
bool window;
bool mouse_hover;
bool audio_dup;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -10,7 +10,8 @@
#include "util/str.h"
bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) {
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
const struct sc_receiver_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
return false;
@ -20,6 +21,10 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) {
receiver->acksync = NULL;
receiver->uhid_devices = NULL;
assert(cbs && cbs->on_ended);
receiver->cbs = cbs;
receiver->cbs_userdata = cbs_userdata;
return true;
}
@ -129,12 +134,15 @@ run_receiver(void *data) {
static uint8_t buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0;
bool error = false;
for (;;) {
assert(head < DEVICE_MSG_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf + head,
DEVICE_MSG_MAX_SIZE - head);
if (r <= 0) {
LOGD("Receiver stopped");
// device disconnected: keep error=false
break;
}
@ -142,6 +150,7 @@ run_receiver(void *data) {
ssize_t consumed = process_msgs(receiver, buf, head);
if (consumed == -1) {
// an error occurred
error = true;
break;
}
@ -152,6 +161,8 @@ run_receiver(void *data) {
}
}
receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata);
return 0;
}

View File

@ -19,10 +19,18 @@ struct sc_receiver {
struct sc_acksync *acksync;
struct sc_uhid_devices *uhid_devices;
const struct sc_receiver_callbacks *cbs;
void *cbs_userdata;
};
struct sc_receiver_callbacks {
void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata);
};
bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket);
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
const struct sc_receiver_callbacks *cbs, void *cbs_userdata);
void
sc_receiver_destroy(struct sc_receiver *receiver);

View File

@ -174,6 +174,9 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_DEMUXER_ERROR:
LOGE("Demuxer error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_CONTROLLER_ERROR:
LOGE("Controller error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
@ -265,6 +268,21 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
}
}
static void
sc_controller_on_ended(struct sc_controller *controller, bool error,
void *userdata) {
// Note: this function may be called twice, once from the controller thread
// and once from the receiver thread
(void) controller;
(void) userdata;
if (error) {
PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR);
} else {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
}
}
static void
sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
(void) server;
@ -376,6 +394,7 @@ scrcpy(struct scrcpy_options *options) {
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.audio_dup = options->audio_dup,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.video_codec_options = options->video_codec_options,
@ -408,7 +427,7 @@ scrcpy(struct scrcpy_options *options) {
return SCRCPY_EXIT_FAILURE;
}
if (options->video_playback) {
if (options->window) {
// Set hints before starting the server thread to avoid race conditions
// in SDL
sdl_set_hints(options->render_driver);
@ -430,7 +449,7 @@ scrcpy(struct scrcpy_options *options) {
assert(!options->video_playback || options->video);
assert(!options->audio_playback || options->audio);
if (options->video_playback ||
if (options->window ||
(options->control && options->clipboard_autosync)) {
// Initialize the video subsystem even if --no-video or
// --no-video-playback is passed so that clipboard synchronization
@ -553,7 +572,12 @@ scrcpy(struct scrcpy_options *options) {
struct sc_mouse_processor *mp = NULL;
if (options->control) {
if (!sc_controller_init(&s->controller, s->server.control_socket)) {
static const struct sc_controller_callbacks controller_cbs = {
.on_ended = sc_controller_on_ended,
};
if (!sc_controller_init(&s->controller, s->server.control_socket,
&controller_cbs, NULL)) {
goto end;
}
controller_initialized = true;
@ -663,7 +687,8 @@ scrcpy(struct scrcpy_options *options) {
}
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller);
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller,
options->mouse_hover);
mp = &s->mouse_sdk.mouse_processor;
} else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) {
bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller);
@ -684,19 +709,20 @@ scrcpy(struct scrcpy_options *options) {
// There is a controller if and only if control is enabled
assert(options->control == !!controller);
if (options->video_playback) {
if (options->window) {
const char *window_title =
options->window_title ? options->window_title : info->device_name;
struct sc_screen_params screen_params = {
.video = options->video_playback,
.controller = controller,
.fp = fp,
.kp = kp,
.mp = mp,
.forward_all_clicks = options->forward_all_clicks,
.mouse_bindings = options->mouse_bindings,
.legacy_paste = options->legacy_paste,
.clipboard_autosync = options->clipboard_autosync,
.shortcut_mods = &options->shortcut_mods,
.shortcut_mods = options->shortcut_mods,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@ -710,20 +736,22 @@ scrcpy(struct scrcpy_options *options) {
.start_fps_counter = options->start_fps_counter,
};
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
}
if (options->audio_playback) {

View File

@ -205,6 +205,8 @@ sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
int dw;
int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &dh);
@ -246,6 +248,8 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->video);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
@ -255,6 +259,13 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
(void) res; // any error already logged
}
static void
sc_screen_render_novideo(struct sc_screen *screen) {
enum sc_display_result res =
sc_display_render(&screen->display, NULL, SC_ORIENTATION_0);
(void) res; // any error already logged
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@ -268,6 +279,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
static int
event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data;
assert(screen->video);
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
// In practice, it seems to always be called from the same thread in
@ -326,6 +339,7 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
static bool
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_screen *screen = DOWNCAST(sink);
assert(screen->video);
bool previous_skipped;
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
@ -364,6 +378,9 @@ sc_screen_init(struct sc_screen *screen,
screen->mouse_capture_key_pressed = 0;
screen->paused = false;
screen->resume_frame = NULL;
screen->orientation = SC_ORIENTATION_0;
screen->video = params->video;
screen->req.x = params->window_x;
screen->req.y = params->window_y;
@ -381,41 +398,75 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_frame_buffer;
}
screen->orientation = params->orientation;
if (screen->orientation != SC_ORIENTATION_0) {
LOGI("Initial display orientation set to %s",
sc_orientation_get_name(screen->orientation));
if (screen->video) {
screen->orientation = params->orientation;
if (screen->orientation != SC_ORIENTATION_0) {
LOGI("Initial display orientation set to %s",
sc_orientation_get_name(screen->orientation));
}
}
uint32_t window_flags = SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_ALLOW_HIGHDPI;
uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
if (params->window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
if (params->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE;
}
const char *title = params->window_title;
assert(title);
int x = SDL_WINDOWPOS_UNDEFINED;
int y = SDL_WINDOWPOS_UNDEFINED;
int width = 256;
int height = 256;
if (params->window_x != SC_WINDOW_POSITION_UNDEFINED) {
x = params->window_x;
}
if (params->window_y != SC_WINDOW_POSITION_UNDEFINED) {
y = params->window_y;
}
if (params->window_width) {
width = params->window_width;
}
if (params->window_height) {
height = params->window_height;
}
// The window will be positioned and sized on first video frame
screen->window =
SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags);
screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
goto error_destroy_fps_counter;
}
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
if (!ok) {
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
scrcpy_icon_destroy(icon);
} else {
} else if (params->video) {
// just a warning
LOGW("Could not load icon");
} else {
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_fps_counter;
}
SDL_Surface *icon_novideo = params->video ? NULL : icon;
bool mipmaps = params->video && params->mipmaps;
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
mipmaps);
if (icon) {
scrcpy_icon_destroy(icon);
}
if (!ok) {
goto error_destroy_window;
}
screen->frame = av_frame_alloc();
@ -430,7 +481,7 @@ sc_screen_init(struct sc_screen *screen,
.screen = screen,
.kp = params->kp,
.mp = params->mp,
.forward_all_clicks = params->forward_all_clicks,
.mouse_bindings = params->mouse_bindings,
.legacy_paste = params->legacy_paste,
.clipboard_autosync = params->clipboard_autosync,
.shortcut_mods = params->shortcut_mods,
@ -439,7 +490,9 @@ sc_screen_init(struct sc_screen *screen,
sc_input_manager_init(&screen->im, &im_params);
#ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, screen);
if (screen->video) {
SDL_AddEventWatch(event_watcher, screen);
}
#endif
static const struct sc_frame_sink_ops ops = {
@ -454,6 +507,11 @@ sc_screen_init(struct sc_screen *screen,
screen->open = false;
#endif
if (!screen->video && sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_screen_set_mouse_capture(screen, true);
}
return true;
error_destroy_display:
@ -524,6 +582,8 @@ sc_screen_destroy(struct sc_screen *screen) {
static void
resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
struct sc_size new_content_size) {
assert(screen->video);
struct sc_size window_size = get_window_size(screen);
struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width
@ -537,6 +597,8 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
assert(screen->video);
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
@ -551,6 +613,8 @@ set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
static void
apply_pending_resize(struct sc_screen *screen) {
assert(screen->video);
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
@ -564,6 +628,8 @@ apply_pending_resize(struct sc_screen *screen) {
void
sc_screen_set_orientation(struct sc_screen *screen,
enum sc_orientation orientation) {
assert(screen->video);
if (orientation == screen->orientation) {
return;
}
@ -598,6 +664,8 @@ sc_screen_init_size(struct sc_screen *screen) {
// recreate the texture and resize the window if the frame size has changed
static enum sc_display_result
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
assert(screen->video);
if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) {
return SC_DISPLAY_RESULT_OK;
@ -617,6 +685,8 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
static bool
sc_screen_apply_frame(struct sc_screen *screen) {
assert(screen->video);
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
AVFrame *frame = screen->frame;
@ -656,6 +726,8 @@ sc_screen_apply_frame(struct sc_screen *screen) {
static bool
sc_screen_update_frame(struct sc_screen *screen) {
assert(screen->video);
if (screen->paused) {
if (!screen->resume_frame) {
screen->resume_frame = av_frame_alloc();
@ -677,6 +749,8 @@ sc_screen_update_frame(struct sc_screen *screen) {
void
sc_screen_set_paused(struct sc_screen *screen, bool paused) {
assert(screen->video);
if (!paused && !screen->paused) {
// nothing to do
return;
@ -704,6 +778,8 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
void
sc_screen_switch_fullscreen(struct sc_screen *screen) {
assert(screen->video);
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
@ -721,6 +797,8 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->maximized || screen->minimized) {
return;
}
@ -745,6 +823,8 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->minimized) {
return;
}
@ -788,6 +868,13 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
return true;
}
case SDL_WINDOWEVENT:
if (!screen->video
&& event->window.event == SDL_WINDOWEVENT_EXPOSED) {
sc_screen_render_novideo(screen);
}
// !video implies !has_frame
assert(screen->video || !screen->has_frame);
if (!screen->has_frame) {
// Do nothing
return true;
@ -891,6 +978,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
struct sc_point
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) {
assert(screen->video);
enum sc_orientation orientation = screen->orientation;
int32_t w = screen->content_size.width;

View File

@ -26,6 +26,8 @@ struct sc_screen {
bool open; // track the open/close state to assert correct behavior
#endif
bool video;
struct sc_display display;
struct sc_input_manager im;
struct sc_frame_buffer fb;
@ -70,15 +72,17 @@ struct sc_screen {
};
struct sc_screen_params {
bool video;
struct sc_controller *controller;
struct sc_file_pusher *fp;
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
const char *window_title;
bool always_on_top;

View File

@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) {
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
return NULL;
}
}
@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) {
case SC_CODEC_RAW:
return "raw";
default:
assert(!"unexpected codec");
return NULL;
}
}
@ -197,6 +198,22 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) {
case SC_CAMERA_FACING_EXTERNAL:
return "external";
default:
assert(!"unexpected camera facing");
return NULL;
}
}
static const char *
sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
switch (audio_source) {
case SC_AUDIO_SOURCE_OUTPUT:
return "output";
case SC_AUDIO_SOURCE_MIC:
return "mic";
case SC_AUDIO_SOURCE_PLAYBACK:
return "playback";
default:
assert(!"unexpected audio source");
return NULL;
}
}
@ -271,8 +288,14 @@ execute_server(struct sc_server *server,
assert(params->video_source == SC_VIDEO_SOURCE_CAMERA);
ADD_PARAM("video_source=camera");
}
if (params->audio_source == SC_AUDIO_SOURCE_MIC) {
ADD_PARAM("audio_source=mic");
// If audio is enabled, an "auto" audio source must have been resolved
assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio);
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) {
ADD_PARAM("audio_source=%s",
sc_server_get_audio_source_name(params->audio_source));
}
if (params->audio_dup) {
ADD_PARAM("audio_dup=true");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);

View File

@ -50,6 +50,7 @@ struct sc_server_params {
uint32_t display_id;
bool video;
bool audio;
bool audio_dup;
bool show_touches;
bool stay_awake;
bool force_adb_forward;

View File

@ -176,6 +176,8 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
free(lpAttributeList);
}
CloseHandle(pi.hThread);
// These handles are used by the child process, close them for this process
if (pin) {
CloseHandle(stdin_read_handle);

View File

@ -169,7 +169,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen,
// .position not used for HID events
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state),
};
assert(mp->ops->process_mouse_motion);
@ -188,8 +188,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen,
// .position not used for HID events
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_click);
@ -208,8 +207,7 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
// .position not used for HID events
.hscroll = event->x,
.vscroll = event->y,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_scroll);

View File

@ -46,6 +46,9 @@ sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) {
uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
if (!can_read) {
return 0;
}
if (samples_count > can_read) {
samples_count = can_read;
}
@ -86,6 +89,9 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size;
if (!can_write) {
return 0;
}
if (samples_count > can_write) {
samples_count = can_write;
}

View File

@ -240,7 +240,7 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
vs->frame = av_frame_alloc();
if (!vs->frame) {
LOG_OOM();
goto error_avcodec_close;
goto error_avcodec_free_context;
}
vs->packet = av_packet_alloc();
@ -268,8 +268,6 @@ error_av_packet_free:
av_packet_free(&vs->packet);
error_av_frame_free:
av_frame_free(&vs->frame);
error_avcodec_close:
avcodec_close(vs->encoder_ctx);
error_avcodec_free_context:
avcodec_free_context(&vs->encoder_ctx);
error_avio_close:
@ -297,7 +295,6 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
av_packet_free(&vs->packet);
av_frame_free(&vs->frame);
avcodec_close(vs->encoder_ctx);
avcodec_free_context(&vs->encoder_ctx);
avio_close(vs->format_ctx->pb);
avformat_free_context(vs->format_ctx);

View File

@ -124,32 +124,22 @@ static void test_options2(void) {
}
static void test_parse_shortcut_mods(void) {
struct sc_shortcut_mods mods;
uint8_t mods;
bool ok;
ok = sc_parse_shortcut_mods("lctrl", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == SC_SHORTCUT_MOD_LCTRL);
ok = sc_parse_shortcut_mods("lctrl+lalt", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_LALT));
assert(mods == SC_SHORTCUT_MOD_LCTRL);
ok = sc_parse_shortcut_mods("rctrl,lalt", &mods);
assert(ok);
assert(mods.count == 2);
assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL);
assert(mods.data[1] == SC_SHORTCUT_MOD_LALT);
assert(mods == (SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_LALT));
ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods);
ok = sc_parse_shortcut_mods("lsuper,rsuper,lctrl", &mods);
assert(ok);
assert(mods.count == 3);
assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER);
assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT));
assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL |
SC_SHORTCUT_MOD_RALT));
assert(mods == (SC_SHORTCUT_MOD_LSUPER
| SC_SHORTCUT_MOD_RSUPER
| SC_SHORTCUT_MOD_LCTRL));
ok = sc_parse_shortcut_mods("", &mods);
assert(!ok);

View File

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

View File

@ -28,14 +28,21 @@ To disable only the audio playback, see [no playback](video.md#no-playback).
## Audio only
To play audio only, disable the video:
To play audio only, disable video and control:
```bash
scrcpy --no-video
scrcpy --no-video --no-control
```
To play audio without a window:
```bash
# --no-video and --no-control are implied by --no-window
scrcpy --no-window
# interrupt with Ctrl+C
```
Without video, the audio latency is typically not criticial, so it might be
Without video, the audio latency is typically not critical, so it might be
interesting to add [buffering](#buffering) to minimize glitches:
```
@ -59,6 +66,30 @@ the computer:
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
```
### Duplication
An alternative device audio capture method is also available (only for Android
13 and above):
```
scrcpy --audio-source=playback
```
This audio source supports keeping the audio playing on the device while
mirroring, with `--audio-dup`:
```bash
scrcpy --audio-source=playback --audio-dup
# or simply:
scrcpy --audio-dup # --audio-source=playback is implied
```
However, it requires Android 13, and Android apps can opt-out (so they are not
captured).
See [#4380](https://github.com/Genymobile/scrcpy/issues/4380).
## Codec

View File

@ -94,7 +94,7 @@ This is the preferred method (and the way the release is built).
From _Debian_, install _mingw_:
```bash
sudo apt install mingw-w64 mingw-w64-tools
sudo apt install mingw-w64 mingw-w64-tools libz-mingw-w64-dev
```
You also need the JDK to build the server:
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v2.4`][direct-scrcpy-server]
<sub>SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3`</sub>
- [`scrcpy-server-v2.5`][direct-scrcpy-server]
<sub>SHA-256: `1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View File

@ -15,6 +15,31 @@ scrcpy -n # short version
Read [keyboard](keyboard.md) and [mouse](mouse.md).
## Control only
To control the device without mirroring:
```bash
scrcpy --no-video --no-audio
```
By default, mouse mode is switched to UHID if video mirroring is disabled (a
relative mouse mode is required).
To also use a UHID keyboard, set it explicitly:
```bash
scrcpy --no-video --no-audio --keyboard=uhid
scrcpy --no-video --no-audio -K # short version
```
To use AOA instead (over USB only):
```bash
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa
```
## Copy-paste
Any time the Android clipboard changes, it is automatically synchronized to the
@ -81,15 +106,6 @@ only inverts _x_.
This only works for the default mouse mode (`--mouse=sdk`).
## Right-click and middle-click
By default, right-click triggers BACK (or POWER on) and middle-click triggers
HOME. To disable these shortcuts and forward the clicks to the device instead:
```bash
scrcpy --forward-all-clicks
```
## File drop
### Install APK

View File

@ -6,7 +6,7 @@
Scrcpy is packaged in several distributions and package managers:
- Debian/Ubuntu: `apt install scrcpy`
- Debian/Ubuntu: ~~`apt install scrcpy`~~ _(obsolete version)_
- Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: `emerge scrcpy`

View File

@ -18,6 +18,14 @@ Note that on some devices, an additional option must be enabled in developer
options for this mouse mode to work. See
[prerequisites](/README.md#prerequisites).
### Mouse hover
By default, mouse hover (mouse motion without any clicks) events are forwarded
to the device. This can be disabled with:
```
scrcpy --no-mouse-hover
```
## Physical mouse simulation
@ -68,3 +76,69 @@ debugging disabled (see [OTG](otg.md)).
Note: On Windows, it may only work in [OTG mode](otg.md), not while mirroring
(it is not possible to open a USB device if it is already open by another
process like the _adb daemon_).
## Mouse bindings
By default, with SDK mouse:
- right-click triggers BACK (or POWER on)
- middle-click triggers HOME
- the 4th click triggers APP_SWITCH
- the 5th click expands the notification panel
The secondary clicks may be forwarded to the device instead by pressing the
<kbd>Shift</kbd> key (e.g. <kbd>Shift</kbd>+right-click injects a right click to
the device).
In AOA and UHID mouse modes, the default bindings are reversed: all clicks are
forwarded by default, and pressing <kbd>Shift</kbd> gives access to the
shortcuts (since the cursor is handled on the device side, it makes more sense
to forward all mouse buttons by default in these modes).
The shortcuts can be configured using `--mouse-bind=xxxx:xxxx` for any mouse
mode. The argument must be one or two sequences (separated by `:`) of exactly 4
characters, one for each secondary click:
```
.---- Shift + right click
SECONDARY |.--- Shift + middle click
BINDINGS ||.-- Shift + 4th click
|||.- Shift + 5th click
||||
vvvv
--mouse-bind=xxxx:xxxx
^^^^
||||
PRIMARY ||| `- 5th click
BINDINGS || `-- 4th click
| `--- middle click
`---- right click
```
Each character must be one of the following:
- `+`: forward the click to the device
- `-`: ignore the click
- `b`: trigger shortcut BACK (or turn screen on if off)
- `h`: trigger shortcut HOME
- `s`: trigger shortcut APP_SWITCH
- `n`: trigger shortcut "expand notification panel"
For example:
```bash
scrcpy --mouse-bind=bhsn:++++ # the default mode for SDK mouse
scrcpy --mouse-bind=++++:bhsn # the default mode for AOA and UHID
scrcpy --mouse-bind=++bh:++sn # forward right and middle clicks,
# use 4th and 5th for BACK and HOME,
# use Shift+4th and Shift+5th for APP_SWITCH
# and expand notification panel
```
The second sequence of bindings may be omitted. In that case, it is the same as
the first one:
```bash
scrcpy --mouse-bind=bhsn
scrcpy --mouse-bind=bhsn:bhsn # equivalent
```

View File

@ -1,19 +1,21 @@
# OTG
By default, _scrcpy_ injects input events at the Android API level. As an
alternative, when connected over USB, it is possible to send HID events, so that
scrcpy behaves as if it was a physical keyboard and/or mouse connected to the
Android device.
alternative, it is possible to send HID events, so that scrcpy behaves as if it
was a [physical keyboard] and/or a [physical mouse] connected to the Android
device (see [keyboard](keyboard.md) and [mouse](mouse.md)).
A special mode allows to control the device without mirroring, using AOA
[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa). Therefore, it is possible
to run _scrcpy_ with only physical keyboard and mouse simulation (HID), as if
the computer keyboard and mouse were plugged directly to the device via an OTG
cable.
[physical keyboard]: keyboard.md#physical-keyboard-simulation
[physical mouse]: physical-keyboard-simulation
In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled.
A special mode (OTG) allows to control the device using AOA
[keyboard](keyboard.md#aoa) and [mouse](mouse.md#aoa), without using _adb_ at
all (so USB debugging is not necessary). In this mode, video and audio are
disabled, and `--keyboard=aoa and `--mouse=aoa` are implicitly set.
This is similar to `--keyboard=aoa --mouse=aoa`, but without mirroring.
Therefore, it is possible to run _scrcpy_ with only physical keyboard and mouse
simulation, as if the computer keyboard and mouse were plugged directly to the
device via an OTG cable.
To enable OTG mode:
@ -23,7 +25,7 @@ scrcpy --otg
scrcpy --otg -s 0123456789abcdef
```
It is possible to disable HID keyboard or HID mouse:
It is possible to disable keyboard or mouse:
```bash
scrcpy --otg --keyboard=disabled
@ -35,3 +37,22 @@ It only works if the device is connected over USB.
## OTG issues on Windows
See [FAQ](/FAQ.md#otg-issues-on-windows).
## Control only
Note that the purpose of OTG is to control the device without USB debugging
(adb).
If you want to solely control the device without mirroring while USB debugging
is enabled, then OTG mode is not necessary.
Instead, disable video and audio, and select UHID (or AOA):
```bash
scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid
scrcpy --no-video --no-audio -KM # short version
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa
```
One benefit of UHID is that it also works wirelessly.

View File

@ -58,12 +58,10 @@ orientation](video.md#orientation).
## No playback
To disable playback while recording:
To disable playback and control while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
scrcpy --no-playback --no-control --record=file.mp4
```
It is also possible to disable video and audio playback separately:
@ -73,6 +71,13 @@ It is also possible to disable video and audio playback separately:
scrcpy --record=file.mkv --no-audio-playback
```
To also disable the window:
```bash
scrcpy --no-playback --no-window --record=file.mp4
# interrupt recording with Ctrl+C
```
## Time limit
To limit the recording time:

View File

@ -13,8 +13,8 @@ It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
# use RCtrl for shortcuts
scrcpy --shortcut-mod=rctrl
# use either LCtrl+LAlt or LSuper for shortcuts
scrcpy --shortcut-mod=lctrl+lalt,lsuper
# use either LCtrl or LSuper for shortcuts
scrcpy --shortcut-mod=lctrl,lsuper
```
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._

View File

@ -1,5 +1,14 @@
# Window
## Disable window
To disable window (may be useful for recording or for playing audio only):
```bash
scrcpy --no-window --record=file.mp4
# Ctrl+C to interrupt
```
## Title
By default, the window title is the device model. It can be changed:

View File

@ -4,24 +4,18 @@
Download the [latest release]:
- [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit)
<sub>SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9`</sub>
- [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit)
<sub>SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116`</sub>
- [`scrcpy-win64-v2.5.zip`][direct-win64] (64-bit)
<sub>SHA-256: `345cf04a66a9144281dce72ca4e82adfd2c3092463196e586051df4c69e1507b`</sub>
- [`scrcpy-win32-v2.5.zip`][direct-win32] (32-bit)
<sub>SHA-256: `d56312a92471565fa4f3a6b94e8eb07717c4c90f2c0f05b03ba444e1001806ec`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win64-v2.4.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win64-v2.5.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-win32-v2.5.zip
and extract it.
Alternatively, you could install it from packages manager, like [Winget]:
```bash
winget install scrcpy
```
or [Chocolatey]:
Alternatively, you could install it from packages manager, like [Chocolatey]:
```bash
choco install scrcpy

View File

@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4
PREBUILT_SERVER_SHA256=93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.5/scrcpy-server-v2.5
PREBUILT_SERVER_SHA256=1488b1105d6aff534873a26bf610cd2aea06ee867dd7a4d9c6bb2c091396eb15
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

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

View File

@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
VERSION := $(shell git describe --tags --always)
VERSION := $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)

View File

@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 34
versionCode 20400
versionName "2.4"
versionCode 20600
versionName "2.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.4
SCRCPY_VERSION_NAME=2.6
PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
@ -50,14 +50,29 @@ cd "$SERVER_DIR/src/main/aidl"
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
SRC=( \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \
)
CLASSES=()
for src in "${SRC[@]}"
do
CLASSES+=("${src%.java}.class")
done
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
${SRC[@]}
echo "Dexing..."
cd "$CLASSES_DIR"
@ -68,8 +83,7 @@ then
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
echo "Archiving..."
cd "$BUILD_DIR"
@ -81,8 +95,7 @@ else
--output "$BUILD_DIR/classes.zip" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"

View File

@ -1,7 +0,0 @@
package com.genymobile.scrcpy;
/**
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
*/
public class AudioCaptureForegroundException extends Exception {
}

View File

@ -1,30 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaRecorder;
public enum AudioSource {
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
MIC("mic", MediaRecorder.AudioSource.MIC);
private final String name;
private final int value;
AudioSource(String name, int value) {
this.name = name;
this.value = value;
}
int value() {
return value;
}
static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View File

@ -1,5 +1,10 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

View File

@ -1,5 +1,15 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.video.CameraAspectRatio;
import com.genymobile.scrcpy.video.CameraFacing;
import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect;
import java.util.List;
@ -16,6 +26,7 @@ public class Options {
private AudioCodec audioCodec = AudioCodec.OPUS;
private VideoSource videoSource = VideoSource.DISPLAY;
private AudioSource audioSource = AudioSource.OUTPUT;
private boolean audioDup;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int maxFps;
@ -90,6 +101,10 @@ public class Options {
return audioSource;
}
public boolean getAudioDup() {
return audioDup;
}
public int getVideoBitRate() {
return videoBitRate;
}
@ -293,6 +308,9 @@ public class Options {
}
options.audioSource = audioSource;
break;
case "audio_dup":
options.audioDup = Boolean.parseBoolean(value);
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;

View File

@ -1,5 +1,29 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCapture;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioDirectCapture;
import com.genymobile.scrcpy.audio.AudioEncoder;
import com.genymobile.scrcpy.audio.AudioPlaybackCapture;
import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
@ -120,7 +144,7 @@ public final class Server {
final Device device = camera ? null : new Device(options);
Workarounds.apply(audio, camera);
Workarounds.apply();
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
@ -142,7 +166,14 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
AudioSource audioSource = options.getAudioSource();
AudioCapture audioCapture;
if (audioSource.isDirect()) {
audioCapture = new AudioDirectCapture(audioSource);
} else {
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
}
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
@ -248,7 +279,7 @@ public final class Server {
Ln.i(LogUtils.buildDisplayListMessage());
}
if (options.getListCameras() || options.getListCameraSizes()) {
Workarounds.apply(false, true);
Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
// Just print the requested data, do not mirror

View File

@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCaptureException;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
@ -48,62 +51,18 @@ public final class Workarounds {
// not instantiable
}
public static void apply(boolean audio, boolean camera) {
boolean mustFillConfigurationController = false;
boolean mustFillAppInfo = false;
boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
mustFillAppInfo = true;
} else if (Build.BRAND.equalsIgnoreCase("honor")) {
// More workarounds must be applied for Honor devices:
// - <https://github.com/Genymobile/scrcpy/issues/4015>
//
// The system context must not be set for all devices, because it would cause other problems:
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
mustFillAppInfo = true;
mustFillAppContext = true;
}
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill the application context for the AudioRecord to work.
mustFillAppContext = true;
}
if (camera) {
mustFillAppInfo = true;
mustFillAppContext = true;
}
public static void apply() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
// <https://github.com/Genymobile/scrcpy/issues/4467>
mustFillConfigurationController = true;
}
if (mustFillConfigurationController) {
// Must be call before fillAppContext() because it is necessary to get a valid system context
// Must be called before fillAppContext() because it is necessary to get a valid system context.
fillConfigurationController();
}
if (mustFillAppInfo) {
fillAppInfo();
}
if (mustFillAppContext) {
fillAppContext();
}
fillAppInfo();
fillAppContext();
}
@SuppressWarnings("deprecation")
@ -191,7 +150,8 @@ public final class Workarounds {
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//
// This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses
@ -332,8 +292,8 @@ public final class Workarounds {
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord.<init>.", e);
throw new RuntimeException("Cannot create AudioRecord");
Ln.e("Cannot create AudioRecord", e);
throw new AudioCaptureException();
}
}
}

View File

@ -0,0 +1,20 @@
package com.genymobile.scrcpy.audio;
import android.media.MediaCodec;
import java.nio.ByteBuffer;
public interface AudioCapture {
void checkCompatibility() throws AudioCaptureException;
void start() throws AudioCaptureException;
void stop();
/**
* Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples.
*
* @param outDirectBuffer The target buffer
* @param outBufferInfo The info to provide to MediaCodec
* @return the number of bytes actually read.
*/
int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo);
}

View File

@ -0,0 +1,12 @@
package com.genymobile.scrcpy.audio;
/**
* Exception for any audio capture issue.
* <p/>
* This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground.
* <p/>
* Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before
* throwing the exception).
*/
public class AudioCaptureException extends Exception {
}

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Codec;
import android.media.MediaFormat;

View File

@ -0,0 +1,29 @@
package com.genymobile.scrcpy.audio;
import android.media.AudioFormat;
public final class AudioConfig {
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private AudioConfig() {
// Not instantiable
}
public static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
}

View File

@ -1,55 +1,48 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
import java.nio.ByteBuffer;
public final class AudioCapture {
public class AudioDirectCapture implements AudioCapture {
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
private static final int CHANNELS = AudioConfig.CHANNELS;
private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
private static final int ENCODING = AudioConfig.ENCODING;
private final int audioSource;
private AudioRecord recorder;
private AudioRecordReader reader;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioCapture(AudioSource audioSource) {
this.audioSource = audioSource.value();
public AudioDirectCapture(AudioSource audioSource) {
this.audioSource = getAudioSourceValue(audioSource);
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
private static int getAudioSourceValue(AudioSource audioSource) {
switch (audioSource) {
case OUTPUT:
return MediaRecorder.AudioSource.REMOTE_SUBMIX;
case MIC:
return MediaRecorder.AudioSource.MIC;
default:
throw new IllegalArgumentException("Unsupported audio source: " + audioSource);
}
}
@TargetApi(Build.VERSION_CODES.M)
@ -61,7 +54,7 @@ public final class AudioCapture {
builder.setContext(FakeContext.get());
}
builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat());
builder.setAudioFormat(AudioConfig.createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
@ -86,7 +79,7 @@ public final class AudioCapture {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
@ -98,7 +91,7 @@ public final class AudioCapture {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
throw new AudioCaptureForegroundException();
throw new AudioCaptureException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
@ -106,7 +99,7 @@ public final class AudioCapture {
}
}
private void startRecording() {
private void startRecording() throws AudioCaptureException {
try {
recorder = createAudioRecord(audioSource);
} catch (NullPointerException e) {
@ -116,9 +109,19 @@ public final class AudioCapture {
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
public void start() throws AudioCaptureForegroundException {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
startWorkaroundAndroid11();
try {
@ -131,6 +134,7 @@ public final class AudioCapture {
}
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
@ -138,42 +142,9 @@ public final class AudioCapture {
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View File

@ -1,4 +1,14 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Streamer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
@ -34,8 +44,8 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
private static final int CHANNELS = AudioCapture.CHANNELS;
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNELS = AudioConfig.CHANNELS;
private final AudioCapture capture;
private final Streamer streamer;
@ -122,7 +132,7 @@ public final class AudioEncoder implements AsyncProcessor {
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
@ -166,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
private void encode() throws IOException, ConfigurationException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
@ -177,6 +187,8 @@ public final class AudioEncoder implements AsyncProcessor {
boolean mediaCodecStarted = false;
try {
capture.checkCompatibility(); // throws an AudioCaptureException on error
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);

View File

@ -0,0 +1,137 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.os.Build;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
public final class AudioPlaybackCapture implements AudioCapture {
private final boolean keepPlayingOnDevice;
private AudioRecord recorder;
private AudioRecordReader reader;
public AudioPlaybackCapture(boolean keepPlayingOnDevice) {
this.keepPlayingOnDevice = keepPlayingOnDevice;
}
@SuppressLint("PrivateApi")
private AudioRecord createAudioRecord() throws AudioCaptureException {
// See <https://github.com/Genymobile/scrcpy/issues/4380>
try {
Class<?> audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule");
Class<?> audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder");
// AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder();
Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance();
// audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS);
int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null);
Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class);
setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant);
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
// audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes);
int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null);
Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class);
addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes);
// AudioMixingRule audioMixingRule = builder.build();
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
// audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true);
Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class);
voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true);
Class<?> audioMixClass = Class.forName("android.media.audiopolicy.AudioMix");
Class<?> audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder");
// AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule);
Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule);
// audioMixBuilder.setFormat(createAudioFormat());
Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class);
setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat());
String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK";
int routeFlags = audioMixClass.getField(routeFlagName).getInt(null);
// audioMixBuilder.setRouteFlags(routeFlag);
Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class);
setRouteFlags.invoke(audioMixBuilder, routeFlags);
// AudioMix audioMix = audioMixBuilder.build();
Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder);
Class<?> audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy");
Class<?> audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder");
// AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder();
Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get());
// audioPolicyBuilder.addMix(audioMix);
Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass);
addMixMethod.invoke(audioPolicyBuilder, audioMix);
// AudioPolicy audioPolicy = audioPolicyBuilder.build();
Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder);
// AudioManager.registerAudioPolicyStatic(audioPolicy);
Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass);
registerAudioPolicyStaticMethod.setAccessible(true);
int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy);
if (result != 0) {
throw new RuntimeException("registerAudioPolicy() returned " + result);
}
// audioPolicy.createAudioRecordSink(audioPolicy);
Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass);
return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix);
} catch (Exception e) {
Ln.e("Could not capture audio playback", e);
throw new AudioCaptureException();
}
}
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
recorder = createAudioRecord();
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
recorder.release();
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View File

@ -1,4 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.os.Build;
@ -18,14 +23,14 @@ public final class AudioRawRecorder implements AsyncProcessor {
this.streamer = streamer;
}
private void record() throws IOException, AudioCaptureForegroundException {
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE);
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try {
@ -64,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
boolean fatalError = false;
try {
record();
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (Throwable t) {
Ln.e("Audio recording error", t);

View File

@ -0,0 +1,67 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer;
public class AudioRecordReader {
private static final long ONE_SAMPLE_US =
(1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private final AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioRecordReader(AudioRecord recorder) {
this.recorder = recorder;
}
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
}
}

View File

@ -0,0 +1,27 @@
package com.genymobile.scrcpy.audio;
public enum AudioSource {
OUTPUT("output"),
MIC("mic"),
PLAYBACK("playback");
private final String name;
AudioSource(String name) {
this.name = name;
}
public boolean isDirect() {
return this != PLAYBACK;
}
public static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import android.net.LocalSocket;

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Position;
/**
* Union of all supported event types, identified by their {@code type}.

View File

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Binary;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Position;
import java.io.EOFException;
import java.io.IOException;

View File

@ -1,5 +1,11 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
@ -22,7 +28,6 @@ public class Controller implements AsyncProcessor {
// control_msg.h values of the pointerId field in inject_touch_event message
private static final int POINTER_ID_MOUSE = -1;
private static final int POINTER_ID_VIRTUAL_MOUSE = -3;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
@ -273,8 +278,8 @@ public class Controller implements AsyncProcessor {
pointer.setPressure(pressure);
int source;
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
if (pointerId == POINTER_ID_MOUSE) {
// real mouse event
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
public final class DeviceMessage {

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;

View File

@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import java.io.IOException;
import java.io.OutputStream;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import java.util.HashMap;
import java.util.Map;

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
public class Pointer {

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
import android.view.MotionEvent;

View File

@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.HandlerThread;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {

View File

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.StringUtils;
import android.net.LocalServerSocket;
import android.net.LocalSocket;

View File

@ -1,5 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.ScreenInfo;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager;
@ -319,10 +323,22 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (applyToMultiPhysicalDisplays
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& Build.BRAND.equalsIgnoreCase("honor")
&& SurfaceControl.hasGetBuildInDisplayMethod()) {
// Workaround for Honor devices with Android 14:
// - <https://github.com/Genymobile/scrcpy/issues/4823>
// - <https://github.com/Genymobile/scrcpy/issues/4943>
applyToMultiPhysicalDisplays = false;
}
if (applyToMultiPhysicalDisplays) {
// On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod();
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod();
// Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public final class DisplayInfo {
private final int displayId;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import android.graphics.Rect;

View File

@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.IO;
import android.media.MediaCodec;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public final class Binary {
private Binary() {

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public interface Codec {

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.util.ArrayList;
import java.util.List;

View File

@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.video.VideoCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.io.IOException;
import java.util.Arrays;

View File

@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import android.os.Handler;

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