Compare commits

...

95 Commits

Author SHA1 Message Date
be9bf7d756 Update documentation for --no-window
PR #4868 <https://github.com/Genymobile/scrcpy/pull/4868>
2024-04-28 16:06:21 +02:00
f7c0826360 Add scrcpy window without video playback
Add the possibility to only control the device with any keyboard and
mouse mode without screen mirroring:

    scrcpy -KM --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-04-28 16:06:21 +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
22d78e8a82 Fix boolean condition
Use the short-circuit operator && between booleans.
2024-04-19 12:49:03 +02:00
bcb8503b26 Handle reported camera sizes array is null
The array of sizes may be null. Handle this case gracefully.

Fixes #4852 <https://github.com/Genymobile/scrcpy/issues/4852>
2024-04-17 10:45:18 +02:00
9aa6cc71be Forbid --no-control in OTG mode
The whole purpose of OTG is to only control the device.
2024-04-16 15:50:44 +02:00
54e08b4eae Fix code style
Limit to 80 columns.
2024-04-16 15:50:41 +02:00
bd8b945bb3 Register rotation watcher only when possible
Old Android versions may not be able to register a rotation watcher for
a secondary display. In that case, report the error instead of
registering a rotation watcher for the default display.

Refs <https://github.com/Genymobile/scrcpy/pull/4740#issuecomment-2051245633>

Suggested by: Kaiming Hu <huxxx1234@gmail.com>
2024-04-12 17:22:45 +02:00
a73bf932d6 Fix could not rotate secondary display
The version of the methods with the display id parameter must be tried
first, otherwise they will never be used (since the old versions without
the display id are still present).

Regression introduced by ee6620d123.

Refs #4740 <https://github.com/Genymobile/scrcpy/pull/4740>
PR #4841 <https://github.com/Genymobile/scrcpy/pull/4841>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-04-12 17:20:15 +02:00
7011dd1ef0 Fix freeze and thaw rotation for Android 14
Changed since AOSP/framework_base commit
670fb7f5c0d23cf51ead25538bcb017e03ed73ac, included in tag
android-14.0.0_r29.

Refs <670fb7f5c0%5E%21/>
PR #4740 <https://github.com/Genymobile/scrcpy/pull/4740>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-04-06 17:38:24 +02:00
ee6620d123 Refactor WindowManager methods
Select the available method to invoke the same way as in other wrappers
(using a version field).

Refs d894e270a7
Refs #4740 <https://github.com/Genymobile/scrcpy/pull/4740>
2024-04-06 17:38:21 +02:00
aa34d63171 Fix segfault on close with --no-video
Do not call sc_screen_hide_window() if screen is not initialized.

To reproduce:

    scrcpy --no-video --record=file.mp4

This only segfaults in debug mode since commit
fd0f432e87.
2024-04-04 08:52:32 +02:00
bf625790fa Request limited color range by default
Most devices currently use limited color range, but some recent devices
encode in full color range, which is currently not supported by the SDL
opengl render driver.

Fixes #4756 <https://github.com/Genymobile/scrcpy/issues/4756>
Refs <https://github.com/Genymobile/scrcpy/issues/4756#issuecomment-2003710860>
Refs libusb/#9311 <https://github.com/libsdl-org/SDL/issues/9311>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-03-30 15:28:57 +01:00
db55edb196 Fix YUV conversion for full color range
Take the color range (full vs limited) into account to render the
picture.

Note that with the current version of SDL, it has no impact with the SDL
opengl render driver.

Fixes #4756 <https://github.com/Genymobile/scrcpy/issues/4756>
Refs <https://github.com/Genymobile/scrcpy/issues/4756#issuecomment-2003228916>
Refs libusb/#9311 <https://github.com/libsdl-org/SDL/issues/9311>

Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-03-30 15:28:09 +01:00
1c3801a0b1 Add a shortcut to pause/unpause display
Pause/unpause display on MOD+z and MOD+Shift+z.

It only impacts rendering, the device is still captured, the video
stream continues to be transmitted to the device and recorded (if
recording is enabled).

Fixes #1632 <https://github.com/Genymobile/scrcpy/issues/1632>
PR #4748 <https://github.com/Genymobile/scrcpy/pull/4748>
2024-03-30 14:20:51 +01:00
be3d357a6d Use source repo tarball for libusb
Legitimate or not, we should not use sources that do not match the
repository.

Refs <https://github.com/libusb/libusb/issues/1468#issuecomment-1974787595>
Refs <https://news.ycombinator.com/item?id=39866309>
Refs #4713 <https://github.com/Genymobile/scrcpy/pull/4713>
2024-03-30 14:18:47 +01:00
79968a0ae6 Reorder documentation
Present the --tcpip option without arguments first.
2024-03-11 18:05:27 +01:00
7f23ff3f2c Add videos for pinch-to-zoom and tilt
A video is worth a thousand words.
2024-03-03 00:06:54 +01:00
cc7719079a Italicize coordinates letters in documentation 2024-03-03 00:05:26 +01:00
0c94b75eef Update links to 2.4 2024-03-03 00:00:24 +01:00
af57309074 Bump version to 2.4 2024-03-02 23:22:09 +01:00
a720c946a6 Merge branch 'master' into release 2024-03-02 23:21:32 +01:00
8d87b91f69 Build dependencies from sources
The project has 3 build dependencies:
 - SDL
 - FFmpeg
 - libusb

For Windows, the release script downloaded pre-built build dependencies
(either from upstream, or from the scrcpy-deps repository).

Instead, download the source releases and build locally. This offers
more flexibility.

The official adb release is still downloaded and included as is in the
release archive (it is not a build dependency).

Also upgrade FFmpeg to 6.1.1 and libusb to 1.0.27.

PR #4713 <https://github.com/Genymobile/scrcpy/pull/4713>
2024-03-02 22:52:54 +01:00
125b1103e1 Happy new year 2024!
PR #4716 <https://github.com/Genymobile/scrcpy/pull/4716>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-03-02 16:48:21 +01:00
36189b90ea Remove spurious line 2024-03-01 09:57:10 +01:00
4dca08cfe3 Set SDL hints before creating any thread
To avoid race conditions in SDL (reported by TSAN).
2024-03-01 09:57:10 +01:00
fd0f432e87 Detect missing initializations
Write invalid data in memory to easily detect missing initializations in
debug mode.
2024-03-01 00:52:35 +01:00
cdf09805c0 Add missing initialization 2024-03-01 00:52:34 +01:00
bf069bd37b Document usage examples
This exposes several common options on the front page.
2024-03-01 00:52:31 +01:00
b9d244b4c9 Document UHID
Rework the documentation to present the keyboard and mouse input modes.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:28 +01:00
dd479ed176 Check options specific to SDK keyboard
Fail if an option specific to --keyboard=sdk is passed with another
keyboard input mode.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:23 +01:00
5f12132c47 Do not fallback keyboard mode if AOA fails
Initially, if AOA initialization failed, default injection method was
used, in order to use the same command/shortcut when the device is
connected via USB or via TCP/IP, without changing the arguments.

Now that there are 3 keyboard modes, it seems unexpected to switch to
another specific mode if AOA fails (and it is inconsistent). If the user
explicitly requests AOA, then use AOA or fail.

Refs #2632 comment <https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-945190859>
PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:19 +01:00
1c5ad0e813 Reassign -K and -M to UHID keyboard and mouse
The options were deprecated, but for convenience, reassign them to
aliases for --keyboard=uhid and --mouse=uhid respectively.

Their long version (--hid-keyboard and --hid-mouse) remain deprecated.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:16 +01:00
6a103c809f Add UHID mouse support
Use the following command:

    scrcpy --mouse=uhid

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:13 +01:00
151a6225d4 Add shortcut to open keyboard settings
The keyboard settings can be opened by:

    adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS

Add a shortcut (MOD+k) for convenience if the current keyboard is HID.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:10 +01:00
54dede3630 Fix startActivity() for supporting API < 30
Call the older startActivityAsUser() instead of
startActivityAsUserWithFeature() so that it also works on older Android
versions.

Fixes #4704 <https://github.com/Genymobile/scrcpy/issues/4704>
PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:05 +01:00
f557188dc8 Create UhidManager only on first use
There is no need to create a UhidManager instance (with its thread) if
no UHID is used.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:52:03 +01:00
87da68ee0d Handle UHID output
Use UHID output reports to synchronize CapsLock and VerrNum states.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-03-01 00:52:00 +01:00
021c5d371a Refactor DeviceMessageSender
Refactor DeviceMessage as a queue of message. This will allow to add
other message types.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:57 +01:00
840680f546 Add UHID keyboard support
Use the following command:

    scrcpy --keyboard=uhid

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-03-01 00:51:54 +01:00
4d5b67cc80 Log controller handling errors
On close, the controller is expected to throw an IOException because the
socket is closed, so the exception was ignored.

However, message handling actions may also throw IOException, and they
must not be silently ignored.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:52 +01:00
604e59ac7b Initialize controller before keyboards
The UHID keyboard initializer will need the controller.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:50 +01:00
4d2c2514fc Initialize controller in two steps
There is a dependency cycle in the initialization order:
 - keyboard depends on controller
 - controller depends on acksync
 - acksync depends on keyboard initialization

To break this cycle, bind the async instance to the controller in a
second step.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:47 +01:00
107f7a83ab Extract binary to hex string conversion
PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:44 +01:00
2e7f6a6fc4 Rename default keyboard implementation to "sdk"
Rename {keyboard,mouse}_inject to {keyboard,mouse}_sdk.

All implementations "inject" key events and mouse events, what differs
is the mechanism. For these implementations, the Android SDK API is used
to inject events.

Note that the input mode enum variants were already renamed
(SC_KEYBOARD_INPUT_MODE_SDK and SC_MOUSE_INPUT_MODE_SDK).

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:41 +01:00
d95276467b Extract mouse HID handling
Split the mouse implementation using AOA and the code handling HID
events, so that HID events can be reused for another protocol (UHID).

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:39 +01:00
91485e2863 Extract keyboard HID handling
Split the keyboard implementation using AOA and the code handling HID
events, so that HID events can be reused for another protocol (UHID).

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:37 +01:00
f2d6203156 Extract HID events struct
An event contained several fields:
 - the accessory id
 - the HID event data
 - a field ack_to_wait specific to the AOA implementation.

Extract the HID event part to prepare the factorization of HID event
creation.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:34 +01:00
2d32557fde Embed HID event data
In the implementation, an HID event is at most 8 bytes. Embed the data
in the HID event structure to avoid allocations and simplify the code.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:30 +01:00
ae303b8d07 Rename hid event "buffer" to "data"
This fields contains the HID event data (there is no "bufferization").

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:29 +01:00
29ce03e337 Rename "buffer" to "data"
The variable name is intended to match the parameter name of
libusb_control_transfer().

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:25 +01:00
48adae1728 Fix HID mouse documentation
The size of a mouse HID event is 4 bytes.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:21 +01:00
ea98d49bae Introduce --keyboard and --mouse
Until now, there was two modes for keyboard and mouse:
 - event injection using the Android system API (default)
 - HID/AOA over USB

For this reason, the options were exposed as simple flags:
 - -K or --hid-keyboard to enable physical keyboard simulation (AOA)
 - -M or --hid-mouse to enable physical mouse simulation (AOA)

Replace them by explicit --keyboard and --mouse options, with 3 possible
values:
 - disabled
 - sdk (default)
 - aoa

This will allow to add a new mode (uhid).

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-03-01 00:51:15 +01:00
35add3daee Accept disabled keyboard or mouse
The input manager assumed that if a controller was present, then both a
key processor and a mouse processor were present.

Remove this assumption, to support disabling keyboard and mouse
separately. This prepares the introduction of new command line options
--keyboard and --mouse.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:51:02 +01:00
c0a1aee8ce Always pass input manager instance
Some functions in input_manager.c only have access to a sub-object (for
example the controller). For consistency, always pass the whole
input manager instance.

This will allow to add assertions when keyboard and mouse could be
disabled separately.

PR #4473 <https://github.com/Genymobile/scrcpy/pull/4473>
2024-03-01 00:50:39 +01:00
a976417572 Fix typo in error message 2024-03-01 00:11:48 +01:00
ffa238b9d3 Remove duplicate lines in libusb script 2024-02-29 23:55:44 +01:00
f6459dd742 Fix FAQ link
Refs ad05a01800
2024-02-29 10:31:45 +01:00
295102a6d9 Check device messages assumptions at runtime
Do not assume the server behaves correctly (scrcpy should not require
the device to be trusted).
2024-02-27 10:06:27 +01:00
d894e270a7 Add rotation support for non-default display
Use new methods introduced by this commit:
<90c9005e68%5E%21/>

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-02-26 20:26:56 +01:00
746eaea556 Add missing clipboard workaround for IQOO device
The first part of the workaround fixed getPrimaryClip(). This part fixes
setPrimaryClip().

Fixes #4703 <https://github.com/Genymobile/scrcpy/issues/4703>
Refs 5ce8672ebc
Refs #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2024-02-26 08:36:18 +01:00
78a7e4f293 Use sc_ prefix for device sender 2024-02-23 20:07:52 +01:00
9858eff856 Fix device message deserialization checks
If any message is incomplete, the deserialization method must return
immediately.
2024-02-23 20:07:52 +01:00
9e22f3bf1c Replace unsigned char by uint8_t for buffers
For consistency.
2024-02-23 20:07:52 +01:00
25f1e703b7 Extract ControlChannel class
This prevents many components from depending on the whole
DesktopConnection.
2024-02-23 20:07:16 +01:00
a7cf4daf3b Avoid negative average buffering
The assumption that underflow and overbuffering are caused by jitter
(and that the delay between the producer and consumer will be caught up)
does not always hold.

For example, if the consumer does not consume at the expected rate (the
SDL callback is not called often enough, which is an audio output
issue), many samples will be dropped due to overbuffering, decreasing
the average buffering indefinitely.

Prevent the average buffering to become negative to limit the
consequences of an unexpected behavior.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:09:38 +01:00
c12fdf900f Minimize buffer underflow on starting
If playback starts too early, insert silence until the buffer is filled
up to at least target_buffering before playing.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:33 +01:00
4502126e3b Use early return to avoid additional indentation
PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:30 +01:00
dfa3f97a87 Fix audio player comment
PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:05:22 +01:00
edac4b8a9a Increase buffering level smoothness
The buffering level does not change continuously: it increases abruptly
when a packet is received, and decreases abruptly when an audio block is
consumed.

To estimate the buffering level, a rolling average is used.

To make the buffering more stable, increase the smoothness of this
rolling average. This decreases the risk of enabling audio compensation
due to an estimation error.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:03:10 +01:00
44abed5c68 Improve audio compensation thresholds
Use different thresholds for enabling and disabling compensation.

Concretely, enable compensation if the difference between the average
and the target buffering levels exceeds 4 ms (instead of 1 ms). This
avoids unnecessary compensation due to small noise in buffering level
estimation.

But keep a smaller threshold (1 ms) for disabling compensation, so that
the buffering level is restored closer to the target value. This avoids
to keep the actual level close to the compensation threshold.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:02:10 +01:00
cfa4f7e2f2 Replace locks by atomics in audio player
The audio output thread only reads samples from the buffer, and most of
the time, the audio receiver thread only writes samples to the buffer.
In these cases, using atomics avoids lock contention.

There are still corner cases where the audio receiver thread needs to
"read" samples (and drop them), so lock only in these cases.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 16:01:33 +01:00
d47ecef1b5 Limit buffering time value
This avoids unreasonable values which could lead to integer overflow.

PR #4572 <https://github.com/Genymobile/scrcpy/pull/4572>
2024-02-17 15:59:58 +01:00
9efa162949 Configure clean up actions dynamically
Some actions may be performed when scrcpy exits, currently:
 - disable "show touches"
 - restore "stay on while plugged in"
 - power off screen
 - restore "power mode" (to disable "turn screen off")

They are performed from a separate process so that they can be executed
even when scrcpy-server is killed (e.g. if the device is unplugged).

The clean up actions to perform were configured when scrcpy started.
Given that there is no method to read the current "power mode" in
Android, and that "turn screen off" can be applied at any time using an
scrcpy shortcut, there was no way to determine if "power mode" had to be
restored on exit. Therefore, it was always restored to "normal", even
when not necessary.

However, setting the "power mode" is quite fragile on some devices, and
may cause some issues, so it is preferable to call it only when
necessary (when "turn screen off" has actually been called).

For that purpose, make the scrcpy-server main process and the clean up
process communicate the actions to perform over a pipe (stdin/stdout),
so that they can be changed dynamically. In particular, when the power
mode is changed at runtime, notify the clean up process.

Refs 1beec99f82
Refs #4456 <https://github.com/Genymobile/scrcpy/issues/4456>
Refs #4624 <https://github.com/Genymobile/scrcpy/issues/4624>
PR #4649 <https://github.com/Genymobile/scrcpy/pull/4649>
2024-02-17 15:49:08 +01:00
be3f949aa5 Adapt to display API changes
The method SurfaceControl.createDisplay() has been removed in AOSP.

Use DisplayManager to create a VirtualDisplay object instead.

Fixes #4646 <https://github.com/Genymobile/scrcpy/issues/4646>
Fixes #4656 <https://github.com/Genymobile/scrcpy/issues/4656>
PR #4657 <https://github.com/Genymobile/scrcpy/pull/4657>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-02-10 10:01:22 +01:00
f7b4a18b43 Catch generic ReflectiveOperationException
This exception is a super-type of:
 - ClassNotFoundException
 - IllegalAccessException
 - InstantiationException
 - InvocationTargetException
 - NoSuchFieldException
 - NoSuchMethodException

Use it to simplify.
2024-02-10 10:00:28 +01:00
05b5deacad Move service managers creation
Create the service managers from each manager wrapper class rather than
from their getter in ServiceManager.

The way a wrapper retrieve the underlying service is an implementation
detail, and it must be consistent with the way it accesses it, so it is
better to write the creation in the wrapper.
2024-02-10 10:00:26 +01:00
d25cbc55f2 Remove unused field 2024-02-09 18:32:48 +01:00
3333e67452 Fix memory leak on error
Fixes #4636 <https://github.com/Genymobile/scrcpy/issues/4636>
2024-02-01 09:19:47 +01:00
7c53a29d72 Remove useless run script
This script was outdated and redundant with ./run.
2024-01-26 13:13:55 +01:00
5187f7254e Add another clipboard workaround for IQOO device
Fixes #4589 <https://github.com/Genymobile/scrcpy/issues/4589>
Refs 5ce8672ebc
Refs #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2024-01-17 10:15:00 +01:00
2ad93d1fc0 Fix scrcpy_otg() return value on error
The function now returns an enum scrcpy_exit_code, not a bool.
2024-01-15 22:01:19 +01:00
d067a11478 Do not power on if no video
Power on the device on start only if video capture is enabled.

Note that it only impacts display mirroring, since control is completely
disabled if video source is camera.

Refs 110b3a16f6
2024-01-07 21:12:39 +01:00
cd4056d0f3 Fix include formatting 2024-01-02 10:22:28 +01:00
6a58891e13 Use current time as initial timestamp on error
If the initial timestamp could not be retrieved, use the current time as
returned by System.nanoTime(). In practice, it is the same time base as
AudioRecord timestamps.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:17:33 +01:00
ec41896c85 Fix integer overflow for audio packet duration
The result is assigned to a long (64-bit signed integer), but the
intermediate multiplication was stored in an int (32-bit signed
integer).

This value is only used as a fallback when no timestamp could be
retrieved, that's why it did not cause too much harm so far.

Fixes #4536 <https://github.com/Genymobile/scrcpy/issues/4536>
2023-12-16 20:16:31 +01:00
4cd61b5a90 Fix checkstyle violation
Reported by checkstyle:

> [ant:checkstyle] [INFO]
> scrcpy/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java:48:
> Line is longer than 150 characters (found 167). [LineLength]
2023-12-16 20:12:58 +01:00
d2ed4510a7 Simulate tilt multitouch event by pressing Shift
PR #4529 <https://github.com/Genymobile/scrcpy/pull/4529>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-15 22:12:07 +01:00
604dfd7c6b Fix incorrect compgen usage
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:08:19 +01:00
af69689ec1 Fix bash completion syntax
PR #4532 <https://github.com/Genymobile/scrcpy/pull/4532>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-12-14 17:07:20 +01:00
cbce42336d Fix manpage syntax
The '-' character must be escaped.

Fixes #4528 <https://github.com/Genymobile/scrcpy/issues/4528>
2023-12-13 13:42:57 +01:00
c9a4d2b38f Use up-to-date values on display fold change
When a display is folded or unfolded, the maxSize may have been updated
since the option was passed, and deviceSize must be updated.

Refs #4469 <https://github.com/Genymobile/scrcpy/pull/4469>
2023-12-04 08:50:12 +01:00
1beec99f82 Explicitly exit cleanup process
This avoids an internal crash reported in `adb logcat`.

Refs #4456 <https://github.com/Genymobile/scrcpy/pull/4456#issuecomment-1837427802>
2023-12-04 08:45:35 +01:00
5ce8672ebc Add clipboard workaround for IQOO device
Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-04 08:43:31 +01:00
3001f8a2d5 Adapt AudioRecord workaround to Android 14
Android 14 added a new int parameter "halInputFlags" to an internal
method:
<f6135d75db>

Fixes #4492 <https://github.com/Genymobile/scrcpy/issues/4492>
2023-12-03 18:01:11 +01:00
129 changed files with 4529 additions and 2398 deletions

11
FAQ.md
View File

@ -133,9 +133,9 @@ Try with another USB cable or plug it into another USB port. See [#281] and
[#283]: https://github.com/Genymobile/scrcpy/issues/283 [#283]: https://github.com/Genymobile/scrcpy/issues/283
## HID/OTG issues on Windows ## OTG issues on Windows
On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in: On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in:
> ERROR: Could not find any USB device > ERROR: Could not find any USB device
@ -170,12 +170,13 @@ The default text injection method is [limited to ASCII characters][text-input].
A trick allows to also inject some [accented characters][accented-characters], A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37]. but that's all. See [#37].
It is also possible to simulate a [physical keyboard][hid] (HID). To avoid the problem, [change the keyboard mode to simulate a physical
keyboard][hid].
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37 [#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: doc/hid-otg.md [hid]: doc/keyboard.md#physical-keyboard-simulation
## Client issues ## Client issues
@ -222,7 +223,7 @@ java.lang.IllegalStateException
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method) at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
``` ```
then try with another [encoder](doc/video.md#codec). then try with another [encoder](doc/video.md#encoder).
## Translations ## Translations

View File

@ -188,7 +188,7 @@
identification within third-party archives. identification within third-party archives.
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2023 Romain Vimont Copyright (C) 2018-2024 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.** their name contains `scrcpy`.**
# scrcpy (v2.3.1) # scrcpy (v2.4)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -36,10 +36,13 @@ Its features include:
- [configurable quality](doc/video.md) - [configurable quality](doc/video.md)
- [camera mirroring](doc/camera.md) (Android 12+) - [camera mirroring](doc/camera.md) (Android 12+)
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
- [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) - physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID)
- [OTG mode](doc/hid-otg.md#otg) - [OTG mode](doc/otg.md)
- and more… - and more…
[hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation
[hid-mouse]: doc/mouse.md#physical-mouse-simulation
## Prerequisites ## Prerequisites
The Android device requires at least API 21 (Android 5.0). The Android device requires at least API 21 (Android 5.0).
@ -57,8 +60,7 @@ this option is set.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
Note that USB debugging is not required to run scrcpy in [OTG Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
mode](doc/hid-otg.md#otg).
## Get the app ## Get the app
@ -68,6 +70,41 @@ mode](doc/hid-otg.md#otg).
- [macOS](doc/macos.md) - [macOS](doc/macos.md)
## Usage examples
There are a lot of options, [documented](#user-documentation) in separate pages.
Here are just some common examples.
- Capture the screen in H.265 (better quality), limit the size to 1920, limit
the frame rate to 60fps, disable audio, and control the device by simulating
a physical keyboard:
```bash
scrcpy --video-codec=h265 --max-size=1920 --max-fps=60 --no-audio --keyboard=uhid
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version
```
- Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
file:
```bash
scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4
```
- Capture the device front camera and expose it as a webcam on the computer (on
Linux):
```bash
scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback
```
- Control the device without mirroring by simulating a physical keyboard and
mouse (USB debugging not required):
```bash
scrcpy --otg
```
## User documentation ## User documentation
The application provides a lot of features and configuration options. They are The application provides a lot of features and configuration options. They are
@ -77,11 +114,13 @@ documented in the following pages:
- [Video](doc/video.md) - [Video](doc/video.md)
- [Audio](doc/audio.md) - [Audio](doc/audio.md)
- [Control](doc/control.md) - [Control](doc/control.md)
- [Keyboard](doc/keyboard.md)
- [Mouse](doc/mouse.md)
- [Device](doc/device.md) - [Device](doc/device.md)
- [Window](doc/window.md) - [Window](doc/window.md)
- [Recording](doc/recording.md) - [Recording](doc/recording.md)
- [Tunnels](doc/tunnels.md) - [Tunnels](doc/tunnels.md)
- [HID/OTG](doc/hid-otg.md) - [OTG](doc/otg.md)
- [Camera](doc/camera.md) - [Camera](doc/camera.md)
- [Video4Linux](doc/v4l2.md) - [Video4Linux](doc/v4l2.md)
- [Shortcuts](doc/shortcuts.md) - [Shortcuts](doc/shortcuts.md)
@ -134,7 +173,7 @@ work][donate]:
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2023 Romain Vimont Copyright (C) 2018-2024 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -27,8 +27,9 @@ _scrcpy() {
--force-adb-forward --force-adb-forward
--forward-all-clicks --forward-all-clicks
-h --help -h --help
-K
--keyboard=
--kill-adb-on-close --kill-adb-on-close
-K --hid-keyboard
--legacy-paste --legacy-paste
--list-camera-sizes --list-camera-sizes
--list-cameras --list-cameras
@ -37,8 +38,9 @@ _scrcpy() {
--lock-video-orientation --lock-video-orientation
--lock-video-orientation= --lock-video-orientation=
-m --max-size= -m --max-size=
-M --hid-mouse -M
--max-fps= --max-fps=
--mouse=
-n --no-control -n --no-control
-N --no-playback -N --no-playback
--no-audio --no-audio
@ -115,13 +117,20 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'front back external' -- "$cur")) COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
return return
;; ;;
--orientation --keyboard)
--display-orientation) COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return
;;
--mouse)
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
return
;;
--orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return return
;; ;;
--record-orientation) --record-orientation)
COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation) --lock-video-orientation)

View File

@ -34,8 +34,9 @@ arguments=(
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]' '--forward-all-clicks[Forward clicks to device]'
{-h,--help}'[Print the help]' {-h,--help}'[Print the help]'
'-K[Use UHID keyboard (same as --keyboard=uhid)]'
'--keyboard[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
'--kill-adb-on-close[Kill adb when scrcpy terminates]' '--kill-adb-on-close[Kill adb when scrcpy terminates]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-camera-sizes[List the valid camera capture sizes]' '--list-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]' '--list-cameras[List cameras available on the device]'
@ -43,8 +44,9 @@ arguments=(
'--list-encoders[List video and audio encoders available on the device]' '--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
{-m,--max-size=}'[Limit both the width and height of the video to value]' {-m,--max-size=}'[Limit both the width and height of the video to value]'
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' '-M[Use UHID mouse (same as --mouse=uhid)]'
'--max-fps=[Limit the frame rate of screen capture]' '--max-fps=[Limit the frame rate of screen capture]'
'--mouse[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]' {-N,--no-playback}'[Disable video and audio playback]'
'--no-audio[Disable audio forwarding]' '--no-audio[Disable audio forwarding]'

1
app/deps/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/work

27
app/deps/README Normal file
View File

@ -0,0 +1,27 @@
This directory (app/deps/) contains:
*.sh : shell scripts to download and build dependencies
patches/ : patches to fix dependencies (used by scripts)
work/sources/ : downloaded tarballs and extracted folders
ffmpeg-6.1.1.tar.xz
ffmpeg-6.1.1/
libusb-1.0.27.tar.gz
libusb-1.0.27/
...
work/build/ : build dirs for each dependency/version/architecture
ffmpeg-6.1.1/win32/
ffmpeg-6.1.1/win64/
libusb-1.0.27/win32/
libusb-1.0.27/win64/
...
work/install/ : install dirs for each architexture
win32/bin/
win32/include/
win32/lib/
win32/share/
win64/bin/
win64/include/
win64/lib/
win64/share/

32
app/deps/adb.sh Executable file
View File

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

55
app/deps/common Normal file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# This file is intended to be sourced by other scripts, not executed
if [[ $# != 1 ]]
then
# <host>: win32 or win64
echo "Syntax: $0 <host>" >&2
exit 1
fi
HOST="$1"
if [[ "$HOST" = win32 ]]
then
HOST_TRIPLET=i686-w64-mingw32
elif [[ "$HOST" = win64 ]]
then
HOST_TRIPLET=x86_64-w64-mingw32
else
echo "Unsupported host: $HOST" >&2
exit 1
fi
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
PATCHES_DIR="$PWD/patches"
WORK_DIR="$PWD/work"
SOURCES_DIR="$WORK_DIR/sources"
BUILD_DIR="$WORK_DIR/build"
INSTALL_DIR="$WORK_DIR/install"
mkdir -p "$INSTALL_DIR" "$SOURCES_DIR" "$WORK_DIR"
checksum() {
local file="$1"
local sum="$2"
echo "$file: verifying checksum..."
echo "$sum $file" | sha256sum -c
}
get_file() {
local url="$1"
local file="$2"
local sum="$3"
if [[ -f "$file" ]]
then
echo "$file: found"
else
echo "$file: not found, downloading..."
wget "$url" -O "$file"
fi
checksum "$file" "$sum"
}

91
app/deps/ffmpeg.sh Executable file
View File

@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=6.1.1
FILENAME=ffmpeg-$VERSION.tar.xz
PROJECT_DIR=ffmpeg-$VERSION
SHA256SUM=8684f4b00f94b85461884c3719382f1261f0d9eb3d59640a1f4ac0873616f968
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
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"
cd "$BUILD_DIR/$PROJECT_DIR"
if [[ "$HOST" = win32 ]]
then
ARCH=x86
elif [[ "$HOST" = win64 ]]
then
ARCH=x86_64
else
echo "Unsupported host: $HOST" >&2
exit 1
fi
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
# -static to avoid dynamic dependency to zlib
export CFLAGS='-static-libgcc -static'
export CXXFLAGS="$CFLAGS"
export LDFLAGS='-static-libgcc -static'
if [[ -d "$HOST" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--enable-cross-compile \
--target-os=mingw32 \
--arch="$ARCH" \
--cross-prefix="${HOST_TRIPLET}-" \
--cc="${HOST_TRIPLET}-gcc" \
--extra-cflags="-O2 -fPIC" \
--enable-shared \
--disable-static \
--disable-programs \
--disable-doc \
--disable-swscale \
--disable-postproc \
--disable-avfilter \
--disable-avdevice \
--disable-network \
--disable-everything \
--enable-swresample \
--enable-decoder=h264 \
--enable-decoder=hevc \
--enable-decoder=av1 \
--enable-decoder=pcm_s16le \
--enable-decoder=opus \
--enable-decoder=aac \
--enable-decoder=flac \
--enable-decoder=png \
--enable-protocol=file \
--enable-demuxer=image2 \
--enable-parser=png \
--enable-zlib \
--enable-muxer=matroska \
--enable-muxer=mp4 \
--enable-muxer=opus \
--enable-muxer=flac \
--enable-muxer=wav \
--disable-vulkan
fi
make -j
make install

45
app/deps/libusb.sh Executable file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=1.0.27
FILENAME=libusb-$VERSION.tar.gz
PROJECT_DIR=libusb-$VERSION
SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS"
if [[ -d "$HOST" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--host="$HOST_TRIPLET" \
--enable-shared \
--disable-static
fi
make -j
make install-strip

View File

@ -0,0 +1,27 @@
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

47
app/deps/sdl.sh Executable file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env bash
set -ex
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DEPS_DIR"
. common
VERSION=2.28.5
FILENAME=SDL-$VERSION.tar.gz
PROJECT_DIR=SDL-release-$VERSION
SHA256SUM=9f0556e4a24ef5b267010038ad9e9948b62f236d5bcc4b22179f95ef62d84023
cd "$SOURCES_DIR"
if [[ -d "$PROJECT_DIR" ]]
then
echo "$PWD/$PROJECT_DIR" found
else
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
fi
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
cd "$BUILD_DIR/$PROJECT_DIR"
export CFLAGS='-O2'
export CXXFLAGS="$CFLAGS"
if [[ -d "$HOST" ]]
then
echo "'$PWD/$HOST' already exists, not reconfigured"
cd "$HOST"
else
mkdir "$HOST"
cd "$HOST"
"$SOURCES_DIR/$PROJECT_DIR"/configure \
--prefix="$INSTALL_DIR/$HOST" \
--host="$HOST_TRIPLET" \
--enable-shared \
--disable-static
fi
make -j
# There is no "make install-strip"
make install
# Strip manually
${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll"

View File

@ -20,8 +20,8 @@ src = [
'src/fps_counter.c', 'src/fps_counter.c',
'src/frame_buffer.c', 'src/frame_buffer.c',
'src/input_manager.c', 'src/input_manager.c',
'src/keyboard_inject.c', 'src/keyboard_sdk.c',
'src/mouse_inject.c', 'src/mouse_sdk.c',
'src/opengl.c', 'src/opengl.c',
'src/options.c', 'src/options.c',
'src/packet_merger.c', 'src/packet_merger.c',
@ -31,11 +31,16 @@ src = [
'src/screen.c', 'src/screen.c',
'src/server.c', 'src/server.c',
'src/version.c', 'src/version.c',
'src/hid/hid_keyboard.c',
'src/hid/hid_mouse.c',
'src/trait/frame_source.c', 'src/trait/frame_source.c',
'src/trait/packet_source.c', 'src/trait/packet_source.c',
'src/uhid/keyboard_uhid.c',
'src/uhid/mouse_uhid.c',
'src/uhid/uhid_output.c',
'src/util/acksync.c', 'src/util/acksync.c',
'src/util/audiobuf.c',
'src/util/average.c', 'src/util/average.c',
'src/util/bytebuf.c',
'src/util/file.c', 'src/util/file.c',
'src/util/intmap.c', 'src/util/intmap.c',
'src/util/intr.c', 'src/util/intr.c',
@ -88,8 +93,8 @@ usb_support = get_option('usb')
if usb_support if usb_support
src += [ src += [
'src/usb/aoa_hid.c', 'src/usb/aoa_hid.c',
'src/usb/hid_keyboard.c', 'src/usb/keyboard_aoa.c',
'src/usb/hid_mouse.c', 'src/usb/mouse_aoa.c',
'src/usb/scrcpy_otg.c', 'src/usb/scrcpy_otg.c',
'src/usb/screen_otg.c', 'src/usb/screen_otg.c',
'src/usb/usb.c', 'src/usb/usb.c',
@ -212,9 +217,10 @@ if get_option('buildtype') == 'debug'
['test_binary', [ ['test_binary', [
'tests/test_binary.c', 'tests/test_binary.c',
]], ]],
['test_bytebuf', [ ['test_audiobuf', [
'tests/test_bytebuf.c', 'tests/test_audiobuf.c',
'src/util/bytebuf.c', 'src/util/audiobuf.c',
'src/util/memory.c',
]], ]],
['test_cli', [ ['test_cli', [
'tests/test_cli.c', 'tests/test_cli.c',

View File

@ -1 +0,0 @@
/data

View File

@ -1,22 +0,0 @@
PREBUILT_DATA_DIR=data
checksum() {
local file="$1"
local sum="$2"
echo "$file: verifying checksum..."
echo "$sum $file" | sha256sum -c
}
get_file() {
local url="$1"
local file="$2"
local sum="$3"
if [[ -f "$file" ]]
then
echo "$file: found"
else
echo "$file: not found, downloading..."
wget "$url" -O "$file"
fi
checksum "$file" "$sum"
}

View File

@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=platform-tools-34.0.5
FILENAME=platform-tools_r34.0.5-windows.zip
SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://dl.google.com/android/repository/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX=platform-tools
unzip "../$FILENAME" \
"$ZIP_PREFIX"/AdbWinApi.dll \
"$ZIP_PREFIX"/AdbWinUsbApi.dll \
"$ZIP_PREFIX"/adb.exe
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.1-scrcpy-3
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/rom1v/scrcpy-deps/releases/download/$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX=ffmpeg
7z x "../$FILENAME"
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"

View File

@ -1,39 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=1.0.26
DEP_DIR="libusb-$VERSION"
FILENAME="libusb-$VERSION-binaries.7z"
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
7z x "../$FILENAME" \
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
"libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
"libusb-$VERSION-binaries/libusb-MinGW-x64/" \
"libusb-$VERSION-binaries/libusb-MinGW-x64/"
mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" .
mv "libusb-$VERSION-binaries/libusb-MinGW-x64" .
rm -rf "libusb-$VERSION-binaries"
# Rename the dll to get the same library name on all platforms
mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll
mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll

View File

@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=2.28.5
DEP_DIR="SDL2-$VERSION"
FILENAME="SDL2-devel-$VERSION-mingw.tar.gz"
SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
TAR_PREFIX="$DEP_DIR" # root directory inside the tar has the same name
tar xf "../$FILENAME" --strip-components=1 \
"$TAR_PREFIX"/i686-w64-mingw32/bin/SDL2.dll \
"$TAR_PREFIX"/i686-w64-mingw32/include/ \
"$TAR_PREFIX"/i686-w64-mingw32/lib/ \
"$TAR_PREFIX"/x86_64-w64-mingw32/bin/SDL2.dll \
"$TAR_PREFIX"/x86_64-w64-mingw32/include/ \
"$TAR_PREFIX"/x86_64-w64-mingw32/lib/ \

View File

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

View File

@ -124,7 +124,7 @@ Use USB device (if there is exactly one, like adb -d).
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
.TP .TP
.BI "\-\-disable-screensaver" .BI "\-\-disable\-screensaver"
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP .TP
@ -172,24 +172,31 @@ By default, right-click triggers BACK (or POWER on) and middle-click triggers HO
Print this help. Print this help.
.TP .TP
.B \-\-kill\-adb\-on\-close .B \-K
Kill adb when scrcpy terminates. Same as \fB\-\-keyboard=uhid\fR.
.TP .TP
.B \-K, \-\-hid\-keyboard .BI "\-\-keyboard " mode
Simulate a physical keyboard by using HID over AOAv2. Select how to send keyboard inputs to the device.
This provides a better experience for IME users, and allows to generate non-ASCII characters, contrary to the default injection method. Possible values are "disabled", "sdk", "uhid" and "aoa":
It may only work over USB. - "disabled" does not send keyboard inputs to the device.
- "sdk" uses the Android system API to deliver keyboard events to applications.
- "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device.
- "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB.
The keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly: For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing:
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
However, the option is only available when the HID keyboard is enabled (or a physical keyboard is connected). This option is only available when the HID keyboard is enabled (or a physical keyboard is connected).
Also see \fB\-\-hid\-mouse\fR. Also see \fB\-\-mouse\fR.
.TP
.B \-\-kill\-adb\-on\-close
Kill adb when scrcpy terminates.
.TP .TP
.B \-\-legacy\-paste .B \-\-legacy\-paste
@ -230,21 +237,31 @@ Limit both the width and height of the video to \fIvalue\fR. The other dimension
Default is 0 (unlimited). Default is 0 (unlimited).
.TP .TP
.B \-M, \-\-hid\-mouse .B \-M
Simulate a physical mouse by using HID over AOAv2. Same as \fB\-\-mouse=uhid\fR.
In this mode, the computer mouse is captured to control the device directly (relative mouse mode).
LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer.
It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR.
.TP .TP
.BI "\-\-max\-fps " value .BI "\-\-max\-fps " value
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions). Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
.TP
.BI "\-\-mouse " mode
Select how to send mouse inputs to the device.
Possible values are "disabled", "sdk", "uhid" and "aoa":
- "disabled" does not send mouse inputs to the device.
- "sdk" uses the Android system API to deliver mouse events to applications.
- "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device.
- "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB.
In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode).
LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer.
Also see \fB\-\-keyboard\fR.
.TP .TP
.B \-n, \-\-no\-control .B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only). Disable device control (mirror the device in read\-only).
@ -299,6 +316,10 @@ Disable video forwarding.
.B \-\-no\-video\-playback .B \-\-no\-video\-playback
Disable video playback on the computer. Disable video playback on the computer.
.TP
.B \-\-no\-window
Disable scrcpy window. Implies --no-video-playback and --no-control.
.TP .TP
.BI "\-\-orientation " value .BI "\-\-orientation " value
Same as --display-orientation=value --record-orientation=value. Same as --display-orientation=value --record-orientation=value.
@ -560,6 +581,14 @@ Flip display horizontally
.B MOD+Shift+Up, MOD+Shift+Down .B MOD+Shift+Up, MOD+Shift+Down
Flip display vertically Flip display vertically
.TP
.B MOD+z
Pause or re-pause display
.TP
.B MOD+Shift+z
Unpause display
.TP .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
@ -636,13 +665,21 @@ Copy computer clipboard to device, then paste (inject PASTE keycode, Android >=
.B MOD+Shift+v .B MOD+Shift+v
Inject computer clipboard text as a sequence of key events Inject computer clipboard text as a sequence of key events
.TP
.B MOD+k
Open keyboard settings on the device (for HID keyboard only)
.TP .TP
.B MOD+i .B MOD+i
Enable/disable FPS counter (print frames/second in logs) Enable/disable FPS counter (print frames/second in logs)
.TP .TP
.B Ctrl+click-and-move .B Ctrl+click-and-move
Pinch-to-zoom from the center of the screen Pinch-to-zoom and rotate from the center of the screen
.TP
.B Shift+click-and-move
Tilt (slide vertically with two fingers)
.TP .TP
.B Drag & drop APK file .B Drag & drop APK file
@ -689,7 +726,7 @@ Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2018 Genymobile <https://www.genymobile.com> Copyright \(co 2018 Genymobile <https://www.genymobile.com>
Copyright \(co 2018\-2023 Romain Vimont <rom@rom1v.com> Copyright \(co 2018\-2024 Romain Vimont <rom@rom1v.com>
Licensed under the Apache License, Version 2.0. Licensed under the Apache License, Version 2.0.

View File

@ -458,6 +458,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
// in the buffer in a single pass // in the buffer in a single pass
LOGW("Result of \"adb devices -l\" does not fit in 64Kb. " LOGW("Result of \"adb devices -l\" does not fit in 64Kb. "
"Please report an issue."); "Please report an issue.");
free(buf);
return false; return false;
} }

View File

@ -66,8 +66,7 @@ static void SDLCALL
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata; struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_AudioDeviceLock(), so // This callback is called with the lock used by SDL_LockAudioDevice()
// the audiobuf is protected
assert(len_int > 0); assert(len_int > 0);
size_t len = len_int; size_t len = len_int;
@ -77,12 +76,12 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif #endif
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
if (!ap->played) { if (!played) {
// Part of the buffering is handled by inserting initial silence. The uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
// remaining (margin) last samples will be handled by compensation. // Wait until the buffer is filled up to at least target_buffering
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms // before playing
if (buffered_samples + margin < ap->target_buffering) { if (buffered_samples < ap->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", count); " samples", count);
// Delay playback starting to reach the target buffering. Fill the // Delay playback starting to reach the target buffering. Fill the
@ -93,10 +92,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
} }
} }
uint32_t read = MIN(buffered_samples, count); uint32_t read = sc_audiobuf_read(&ap->buf, stream, count);
if (read) {
sc_audiobuf_read(&ap->buf, stream, read);
}
if (read < count) { if (read < count) {
uint32_t silence = count - read; uint32_t silence = count - read;
@ -109,13 +105,16 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
silence); silence);
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
if (ap->received) { bool received = atomic_load_explicit(&ap->received,
memory_order_relaxed);
if (received) {
// Inserting additional samples immediately increases buffering // Inserting additional samples immediately increases buffering
ap->underflow += silence; atomic_fetch_add_explicit(&ap->underflow, silence,
memory_order_relaxed);
} }
} }
ap->played = true; atomic_store_explicit(&ap->played, true, memory_order_relaxed);
} }
static uint8_t * static uint8_t *
@ -162,155 +161,168 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// swr_convert() returns the number of samples which would have been // swr_convert() returns the number of samples which would have been
// written if the buffer was big enough. // written if the buffer was big enough.
uint32_t samples_written = MIN(ret, dst_nb_samples); uint32_t samples = MIN(ret, dst_nb_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written); LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif #endif
// Since this function is the only writer, the current available space is uint32_t cap = sc_audiobuf_capacity(&ap->buf);
// at least the previous available space. In practice, it should almost if (samples > cap) {
// always be possible to write without lock. // Very very unlikely: a single resampled frame should never
bool lockless_write = samples_written <= ap->previous_can_write; // exceed the audio buffer size (or something is very wrong).
if (lockless_write) { // Ignore the first bytes in swr_buf to avoid memory corruption anyway.
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written); swr_buf += TO_BYTES(samples - cap);
samples = cap;
} }
SDL_LockAudioDevice(ap->device); uint32_t skipped_samples = 0;
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples);
if (written < samples) {
uint32_t remaining = samples - written;
if (lockless_write) { // All samples that could be written without locking have been written,
sc_audiobuf_commit_write(&ap->buf, samples_written); // now we need to lock to drop/consume old samples
} else { SDL_LockAudioDevice(ap->device);
uint32_t can_write = sc_audiobuf_can_write(&ap->buf);
if (samples_written > can_write) {
// Entering this branch is very unlikely, the audio buffer is
// allocated with a size sufficient to store 1 second more than the
// target buffering. If this happens, though, we have to skip old
// samples.
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
if (samples_written > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Ignore the first bytes in swr_buf
swr_buf += TO_BYTES(samples_written - cap);
// This change in samples_written will impact the
// instant_compensation below
samples_written = cap;
}
assert(samples_written >= can_write); // Retry with the lock
if (samples_written > can_write) { written += sc_audiobuf_write(&ap->buf,
uint32_t skip_samples = samples_written - can_write; swr_buf + TO_BYTES(written),
assert(buffered_samples >= skip_samples); remaining);
sc_audiobuf_skip(&ap->buf, skip_samples); if (written < samples) {
buffered_samples -= skip_samples; remaining = samples - written;
if (ap->played) { // Still insufficient, drop old samples to make space
// Dropping input samples instantly decreases buffering skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
ap->avg_buffering.avg -= skip_samples; assert(skipped_samples == remaining);
}
}
// It should remain exactly the expected size to write the new // Now there is enough space
// samples. uint32_t w = sc_audiobuf_write(&ap->buf,
assert(sc_audiobuf_can_write(&ap->buf) == samples_written); swr_buf + TO_BYTES(written),
remaining);
assert(w == remaining);
(void) w;
} }
sc_audiobuf_write(&ap->buf, swr_buf, samples_written); SDL_UnlockAudioDevice(ap->device);
} }
buffered_samples += samples_written; uint32_t underflow = 0;
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf)); uint32_t max_buffered_samples;
bool played = atomic_load_explicit(&ap->played, memory_order_relaxed);
// Read with lock held, to be used after unlocking
bool played = ap->played;
uint32_t underflow = ap->underflow;
if (played) { if (played) {
uint32_t max_buffered_samples = ap->target_buffering underflow = atomic_exchange_explicit(&ap->underflow, 0,
+ 12 * ap->output_buffer memory_order_relaxed);
+ ap->target_buffering / 10;
if (buffered_samples > max_buffered_samples) {
uint32_t skip_samples = buffered_samples - max_buffered_samples;
sc_audiobuf_skip(&ap->buf, skip_samples);
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
}
// reset (the current value was copied to a local variable) max_buffered_samples = ap->target_buffering
ap->underflow = 0; + 12 * ap->output_buffer
+ ap->target_buffering / 10;
} else { } else {
// SDL playback not started yet, do not accumulate more than // SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay // max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start. // (and glitches to compensate) on start.
uint32_t max_initial_buffering = ap->target_buffering max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer;
+ 2 * ap->output_buffer; }
if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering; uint32_t can_read = sc_audiobuf_can_read(&ap->buf);
sc_audiobuf_skip(&ap->buf, skip_samples); if (can_read > max_buffered_samples) {
uint32_t skip_samples = 0;
SDL_LockAudioDevice(ap->device);
can_read = sc_audiobuf_can_read(&ap->buf);
if (can_read > max_buffered_samples) {
skip_samples = can_read - max_buffered_samples;
uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples);
assert(r == skip_samples);
(void) r;
skipped_samples += skip_samples;
}
SDL_UnlockAudioDevice(ap->device);
if (skip_samples) {
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples", } else {
skip_samples); LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
#endif #endif
}
} }
} }
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf); atomic_store_explicit(&ap->received, true, memory_order_relaxed);
ap->received = true; if (!played) {
// Nothing more to do
return true;
}
SDL_UnlockAudioDevice(ap->device); // Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
// Inserting silence instantly increases buffering
int32_t inserted_silence = (int32_t) underflow;
// Dropping input samples instantly decreases buffering
int32_t dropped = (int32_t) skipped_samples;
if (played) { // The compensation must apply instantly, it must not be smoothed
// Number of samples added (or removed, if negative) for compensation ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
int32_t instant_compensation = if (ap->avg_buffering.avg < 0) {
(int32_t) samples_written - frame->nb_samples; // Since dropping samples instantly reduces buffering, the difference
int32_t inserted_silence = (int32_t) underflow; // is applied immediately to the average value, assuming that the delay
// between the producer and the consumer will be caught up.
//
// However, when this assumption is not valid, the average buffering
// may decrease indefinitely. Prevent it to become negative to limit
// the consequences.
ap->avg_buffering.avg = 0;
}
// The compensation must apply instantly, it must not be smoothed // However, the buffering level must be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence; sc_average_push(&ap->avg_buffering, can_read);
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, buffered_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG #ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f", LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
buffered_samples, sc_average_get(&ap->avg_buffering)); can_read, sc_average_get(&ap->avg_buffering));
#endif #endif
ap->samples_since_resync += samples_written; ap->samples_since_resync += written;
if (ap->samples_since_resync >= ap->sample_rate) { if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second // Recompute compensation every second
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
float avg = sc_average_get(&ap->avg_buffering); float avg = sc_average_get(&ap->avg_buffering);
int diff = ap->target_buffering - avg; int diff = ap->target_buffering - avg;
if (abs(diff) < (int) ap->sample_rate / 1000) {
// Do not compensate for less than 1ms, the error is just noise
diff = 0;
} else if (diff < 0 && buffered_samples < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below
// the average, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after
// 1 second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg,
buffered_samples, diff);
if (diff != ap->compensation) { // Enable compensation when the difference exceeds +/- 4ms.
int ret = swr_set_compensation(swr_ctx, diff, distance); // Disable compensation when the difference is lower than +/- 1ms.
if (ret < 0) { int threshold = ap->compensation != 0
LOGW("Resampling compensation failed: %d", ret); ? ap->sample_rate / 1000 /* 1ms */
// not fatal : ap->sample_rate * 4 / 1000; /* 4ms */
} else {
ap->compensation = diff; if (abs(diff) < threshold) {
} // Do not compensate for small values, the error is just noise
diff = 0;
} else if (diff < 0 && can_read < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below the
// target, this would increase underflow
diff = 0;
}
// Compensate the diff over 4 seconds (but will be recomputed after 1
// second)
int distance = 4 * ap->sample_rate;
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg, can_read, diff);
if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ap->compensation = diff;
} }
} }
} }
@ -397,7 +409,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
// producer and the consumer. It's too big on purpose, to guarantee that // producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel // the producer and the consumer will be able to access it in parallel
// without locking. // without locking.
size_t audiobuf_samples = ap->target_buffering + ap->sample_rate; uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
@ -413,16 +425,15 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
} }
ap->swr_buf_alloc_size = initial_swr_buf_size; ap->swr_buf_alloc_size = initial_swr_buf_size;
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
// Samples are produced and consumed by blocks, so the buffering must be // Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value. // smoothed to get a relatively stable value.
sc_average_init(&ap->avg_buffering, 32); sc_average_init(&ap->avg_buffering, 128);
ap->samples_since_resync = 0; ap->samples_since_resync = 0;
ap->received = false; ap->received = false;
ap->played = false; atomic_init(&ap->played, false);
ap->underflow = 0; atomic_init(&ap->received, false);
atomic_init(&ap->underflow, 0);
ap->compensation = 0; ap->compensation = 0;
// The thread calling open() is the thread calling push(), which fills the // The thread calling open() is the thread calling push(), which fills the

View File

@ -3,17 +3,18 @@
#include "common.h" #include "common.h"
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include "trait/frame_sink.h"
#include <util/audiobuf.h>
#include <util/average.h>
#include <util/thread.h>
#include <util/tick.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libswresample/swresample.h> #include <libswresample/swresample.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "trait/frame_sink.h"
#include "util/audiobuf.h"
#include "util/average.h"
#include "util/thread.h"
#include "util/tick.h"
struct sc_audio_player { struct sc_audio_player {
struct sc_frame_sink frame_sink; struct sc_frame_sink frame_sink;
@ -32,13 +33,9 @@ struct sc_audio_player {
uint16_t output_buffer; uint16_t output_buffer;
// Audio buffer to communicate between the receiver and the SDL audio // Audio buffer to communicate between the receiver and the SDL audio
// callback (protected by SDL_AudioDeviceLock()) // callback
struct sc_audiobuf buf; struct sc_audiobuf buf;
// The previous empty space in the buffer (only used by the receiver
// thread)
uint32_t previous_can_write;
// Resampler (only used from the receiver thread) // Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx; struct SwrContext *swr_ctx;
@ -47,7 +44,7 @@ struct sc_audio_player {
// The number of channels is the same for input and output // The number of channels is the same for input and output
unsigned nb_channels; unsigned nb_channels;
// The number of bytes per sample for a single channel // The number of bytes per sample for a single channel
unsigned out_bytes_per_sample; size_t out_bytes_per_sample;
// Target buffer for resampling (only used by the receiver thread) // Target buffer for resampling (only used by the receiver thread)
uint8_t *swr_buf; uint8_t *swr_buf;
@ -61,19 +58,16 @@ struct sc_audio_player {
uint32_t samples_since_resync; uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet // Number of silence samples inserted since the last received packet
// (protected by SDL_AudioDeviceLock()) atomic_uint_least32_t underflow;
uint32_t underflow;
// Current applied compensation value (only used by the receiver thread) // Current applied compensation value (only used by the receiver thread)
int compensation; int compensation;
// Set to true the first time a sample is received (protected by // Set to true the first time a sample is received
// SDL_AudioDeviceLock()) atomic_bool received;
bool received;
// Set to true the first time the SDL callback is called (protected by // Set to true the first time the SDL callback is called
// SDL_AudioDeviceLock()) atomic_bool played;
bool played;
const struct sc_audio_player_callbacks *cbs; const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;

View File

@ -93,6 +93,11 @@ enum {
OPT_DISPLAY_ORIENTATION, OPT_DISPLAY_ORIENTATION,
OPT_RECORD_ORIENTATION, OPT_RECORD_ORIENTATION,
OPT_ORIENTATION, OPT_ORIENTATION,
OPT_KEYBOARD,
OPT_MOUSE,
OPT_HID_KEYBOARD_DEPRECATED,
OPT_HID_MOUSE_DEPRECATED,
OPT_NO_WINDOW,
}; };
struct sc_option { struct sc_option {
@ -358,27 +363,44 @@ static const struct sc_option options[] = {
.longopt = "help", .longopt = "help",
.text = "Print this help.", .text = "Print this help.",
}, },
{
.shortopt = 'K',
.text = "Same as --keyboard=uhid.",
},
{
.longopt_id = OPT_KEYBOARD,
.longopt = "keyboard",
.argdesc = "mode",
.text = "Select how to send keyboard inputs to the device.\n"
"Possible values are \"disabled\", \"sdk\", \"uhid\" and "
"\"aoa\".\n"
"\"disabled\" does not send keyboard inputs to the device.\n"
"\"sdk\" uses the Android system API to deliver keyboard "
"events to applications.\n"
"\"uhid\" simulates a physical HID keyboard using the Linux "
"UHID kernel module on the device.\n"
"\"aoa\" simulates a physical keyboard using the AOAv2 "
"protocol. It may only work over USB.\n"
"For \"uhid\" and \"aoa\", the keyboard layout must be "
"configured (once and for all) on the device, via Settings -> "
"System -> Languages and input -> Physical keyboard. This "
"settings page can be started directly using the shortcut "
"MOD+k (except in OTG mode) or by executing: `adb shell am "
"start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n"
"This option is only available when a HID keyboard is enabled "
"(or a physical keyboard is connected).\n"
"Also see --mouse.",
},
{ {
.longopt_id = OPT_KILL_ADB_ON_CLOSE, .longopt_id = OPT_KILL_ADB_ON_CLOSE,
.longopt = "kill-adb-on-close", .longopt = "kill-adb-on-close",
.text = "Kill adb when scrcpy terminates.", .text = "Kill adb when scrcpy terminates.",
}, },
{ {
.shortopt = 'K', // deprecated
//.shortopt = 'K', // old, reassigned
.longopt_id = OPT_HID_KEYBOARD_DEPRECATED,
.longopt = "hid-keyboard", .longopt = "hid-keyboard",
.text = "Simulate a physical keyboard by using HID over AOAv2.\n"
"It provides a better experience for IME users, and allows to "
"generate non-ASCII characters, contrary to the default "
"injection method.\n"
"It may only work over USB.\n"
"The keyboard layout must be configured (once and for all) on "
"the device, via Settings -> System -> Languages and input -> "
"Physical keyboard. This settings page can be started "
"directly: `adb shell am start -a "
"android.settings.HARD_KEYBOARD_SETTINGS`.\n"
"However, the option is only available when the HID keyboard "
"is enabled (or a physical keyboard is connected).\n"
"Also see --hid-mouse.",
}, },
{ {
.longopt_id = OPT_LEGACY_PASTE, .longopt_id = OPT_LEGACY_PASTE,
@ -432,15 +454,14 @@ static const struct sc_option options[] = {
"Default is 0 (unlimited).", "Default is 0 (unlimited).",
}, },
{ {
.shortopt = 'M', // deprecated
//.shortopt = 'M', // old, reassigned
.longopt_id = OPT_HID_MOUSE_DEPRECATED,
.longopt = "hid-mouse", .longopt = "hid-mouse",
.text = "Simulate a physical mouse by using HID over AOAv2.\n" },
"In this mode, the computer mouse is captured to control the " {
"device directly (relative mouse mode).\n" .shortopt = 'M',
"LAlt, LSuper or RSuper toggle the capture mode, to give " .text = "Same as --mouse=uhid.",
"control of the mouse back to the computer.\n"
"It may only work over USB.\n"
"Also see --hid-keyboard.",
}, },
{ {
.longopt_id = OPT_MAX_FPS, .longopt_id = OPT_MAX_FPS,
@ -449,6 +470,26 @@ static const struct sc_option options[] = {
.text = "Limit the frame rate of screen capture (officially supported " .text = "Limit the frame rate of screen capture (officially supported "
"since Android 10, but may work on earlier versions).", "since Android 10, but may work on earlier versions).",
}, },
{
.longopt_id = OPT_MOUSE,
.longopt = "mouse",
.argdesc = "mode",
.text = "Select how to send mouse inputs to the device.\n"
"Possible values are \"disabled\", \"sdk\", \"uhid\" and "
"\"aoa\".\n"
"\"disabled\" does not send mouse inputs to the device.\n"
"\"sdk\" uses the Android system API to deliver mouse events"
"to applications.\n"
"\"uhid\" simulates a physical HID mouse using the Linux UHID "
"kernel module on the device.\n"
"\"aoa\" simulates a physical mouse using the AOAv2 protocol. "
"It may only work over USB.\n"
"In \"uhid\" and \"aoa\" modes, the computer mouse is captured "
"to control the device directly (relative mouse mode).\n"
"LAlt, LSuper or RSuper toggle the capture mode, to give "
"control of the mouse back to the computer.\n"
"Also see --keyboard.",
},
{ {
.shortopt = 'n', .shortopt = 'n',
.longopt = "no-control", .longopt = "no-control",
@ -526,6 +567,12 @@ static const struct sc_option options[] = {
.longopt = "no-video-playback", .longopt = "no-video-playback",
.text = "Disable video playback on the computer.", .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_id = OPT_ORIENTATION,
.longopt = "orientation", .longopt = "orientation",
@ -543,10 +590,10 @@ static const struct sc_option options[] = {
"mirroring is disabled.\n" "mirroring is disabled.\n"
"LAlt, LSuper or RSuper toggle the mouse capture mode, to give " "LAlt, LSuper or RSuper toggle the mouse capture mode, to give "
"control of the mouse back to the computer.\n" "control of the mouse back to the computer.\n"
"If any of --hid-keyboard or --hid-mouse is set, only enable " "Keyboard and mouse may be disabled separately using"
"keyboard or mouse respectively, otherwise enable both.\n" "--keyboard=disabled and --mouse=disabled.\n"
"It may only work over USB.\n" "It may only work over USB.\n"
"See --hid-keyboard and --hid-mouse.", "See --keyboard and --mouse.",
}, },
{ {
.shortopt = 'p', .shortopt = 'p',
@ -860,6 +907,14 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" },
.text = "Flip display vertically", .text = "Flip display vertically",
}, },
{
.shortcuts = { "MOD+z" },
.text = "Pause or re-pause display",
},
{
.shortcuts = { "MOD+Shift+z" },
.text = "Unpause display",
},
{ {
.shortcuts = { "MOD+g" }, .shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)", .text = "Resize window to 1:1 (pixel-perfect)",
@ -941,13 +996,21 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "MOD+Shift+v" }, .shortcuts = { "MOD+Shift+v" },
.text = "Inject computer clipboard text as a sequence of key events", .text = "Inject computer clipboard text as a sequence of key events",
}, },
{
.shortcuts = { "MOD+k" },
.text = "Open keyboard settings on the device (for HID keyboard only)",
},
{ {
.shortcuts = { "MOD+i" }, .shortcuts = { "MOD+i" },
.text = "Enable/disable FPS counter (print frames/second in logs)", .text = "Enable/disable FPS counter (print frames/second in logs)",
}, },
{ {
.shortcuts = { "Ctrl+click-and-move" }, .shortcuts = { "Ctrl+click-and-move" },
.text = "Pinch-to-zoom from the center of the screen", .text = "Pinch-to-zoom and rotate from the center of the screen",
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
}, },
{ {
.shortcuts = { "Drag & drop APK file" }, .shortcuts = { "Drag & drop APK file" },
@ -1381,7 +1444,11 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
static bool static bool
parse_buffering_time(const char *s, sc_tick *tick) { parse_buffering_time(const char *s, sc_tick *tick) {
long value; long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, // In practice, buffering time should not exceed a few seconds.
// Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow
// when multiplied by the audio sample size and the number of samples per
// millisecond.
bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000,
"buffering time"); "buffering time");
if (!ok) { if (!ok) {
return false; return false;
@ -1898,6 +1965,69 @@ parse_camera_fps(const char *s, uint16_t *camera_fps) {
return true; return true;
} }
static bool
parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) {
if (!strcmp(optarg, "disabled")) {
*mode = SC_KEYBOARD_INPUT_MODE_DISABLED;
return true;
}
if (!strcmp(optarg, "sdk")) {
*mode = SC_KEYBOARD_INPUT_MODE_SDK;
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_KEYBOARD_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_KEYBOARD_INPUT_MODE_AOA;
return true;
#else
LOGE("--keyboard=aoa is disabled.");
return false;
#endif
}
LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)",
optarg);
return false;
}
static bool
parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) {
if (!strcmp(optarg, "disabled")) {
*mode = SC_MOUSE_INPUT_MODE_DISABLED;
return true;
}
if (!strcmp(optarg, "sdk")) {
*mode = SC_MOUSE_INPUT_MODE_SDK;
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_MOUSE_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_MOUSE_INPUT_MODE_AOA;
return true;
#else
LOGE("--mouse=aoa is disabled.");
return false;
#endif
}
LOGE("Unsupported mouse: %s (expected disabled, sdk, uhid or aoa)", optarg);
return false;
}
static bool static bool
parse_time_limit(const char *s, sc_tick *tick) { parse_time_limit(const char *s, sc_tick *tick) {
long value; long value;
@ -1986,13 +2116,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
args->help = true; args->help = true;
break; break;
case 'K': case 'K':
#ifdef HAVE_USB opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID;
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_HID;
break; break;
#else case OPT_KEYBOARD:
LOGE("HID over AOA (-K/--hid-keyboard) is disabled."); if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) {
return false;
}
break;
case OPT_HID_KEYBOARD_DEPRECATED:
LOGE("--hid-keyboard has been removed, use --keyboard=aoa or "
"--keyboard=uhid instead.");
return false; return false;
#endif
case OPT_MAX_FPS: case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) { if (!parse_max_fps(optarg, &opts->max_fps)) {
return false; return false;
@ -2004,13 +2138,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
break; break;
case 'M': case 'M':
#ifdef HAVE_USB opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_HID;
break; break;
#else case OPT_MOUSE:
LOGE("HID over AOA (-M/--hid-mouse) is disabled."); if (!parse_mouse(optarg, &opts->mouse_input_mode)) {
return false;
}
break;
case OPT_HID_MOUSE_DEPRECATED:
LOGE("--hid-mouse has been removed, use --mouse=aoa or "
"--mouse=uhid instead.");
return false; return false;
#endif
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, if (!parse_lock_video_orientation(optarg,
&opts->lock_video_orientation)) { &opts->lock_video_orientation)) {
@ -2355,6 +2493,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_CAMERA_HIGH_SPEED: case OPT_CAMERA_HIGH_SPEED:
opts->camera_high_speed = true; opts->camera_high_speed = true;
break; break;
case OPT_NO_WINDOW:
opts->window = false;
break;
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
@ -2392,8 +2533,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
v4l2 = !!opts->v4l2_device; v4l2 = !!opts->v4l2_device;
#endif #endif
if (!opts->window) {
// Without window, there cannot be any video playback or control
opts->video_playback = false;
opts->control = false;
}
if (!opts->video) { if (!opts->video) {
opts->video_playback = false; opts->video_playback = false;
// Do not power on the device on start if video capture is disabled
opts->power_on = false;
} }
if (!opts->audio) { if (!opts->audio) {
@ -2411,8 +2560,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->audio = false; opts->audio = false;
} }
if (!opts->video && !opts->audio && !otg) { if (!opts->video && !opts->audio && !opts->control && !otg) {
LOGE("No video, no audio, no OTG: nothing to do"); LOGE("No video, no audio, no control, no OTG: nothing to do");
return false; return false;
} }
@ -2423,9 +2572,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->audio_playback && opts->audio_buffer == -1) { if (opts->audio_playback && opts->audio_buffer == -1) {
if (opts->audio_codec == SC_CODEC_FLAC) { if (opts->audio_codec == SC_CODEC_FLAC) {
// Use 50 ms audio buffer by default, but use a higher value for FLAC, // Use 50 ms audio buffer by default, but use a higher value for
// which is not low latency (the default encoder produces blocks of // FLAC, which is not low latency (the default encoder produces
// 4096 samples, which represent ~85.333ms). // blocks of 4096 samples, which represent ~85.333ms).
LOGI("FLAC audio: audio buffer increased to 120 ms (use " LOGI("FLAC audio: audio buffer increased to 120 ms (use "
"--audio-buffer to set a custom value)"); "--audio-buffer to set a custom value)");
opts->audio_buffer = SC_TICK_FROM_MS(120); opts->audio_buffer = SC_TICK_FROM_MS(120);
@ -2455,6 +2604,72 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
#endif #endif
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, SDK mouse disabled (you might want "
"--mouse=uhid)");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
} 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 (otg) {
if (!opts->control) {
LOGE("--no-control is not allowed in OTG mode");
return false;
}
enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode;
if (kmode != SC_KEYBOARD_INPUT_MODE_AOA
&& kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --keyboard only supports aoa or disabled.");
return false;
}
enum sc_mouse_input_mode mmode = opts->mouse_input_mode;
if (mmode != SC_MOUSE_INPUT_MODE_AOA
&& mmode != SC_MOUSE_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --mouse only supports aoa or disabled.");
return false;
}
if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED) {
LOGE("Could not disable both keyboard and mouse in OTG mode.");
return false;
}
}
if (opts->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_SDK) {
if (opts->key_inject_mode == SC_KEY_INJECT_MODE_TEXT) {
LOGE("--prefer-text is specific to --keyboard=sdk");
return false;
}
if (opts->key_inject_mode == SC_KEY_INJECT_MODE_RAW) {
LOGE("--raw-key-events is specific to --keyboard=sdk");
return false;
}
if (!opts->forward_key_repeat) {
LOGE("--no-key-repeat is specific to --keyboard=sdk");
return false;
}
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) { if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, " LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled."); "--force-adb-forward automatically enabled.");
@ -2615,16 +2830,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} }
# ifdef _WIN32 # ifdef _WIN32
if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) { || opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA)) {
LOGE("On Windows, it is not possible to open a USB device already open " LOGE("On Windows, it is not possible to open a USB device already open "
"by another process (like adb)."); "by another process (like adb).");
LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in " LOGE("Therefore, --keyboard=aoa and --mouse=aoa may only work in OTG"
"OTG mode (--otg)."); "mode (--otg).");
return false; return false;
} }
# endif # 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) { if (otg) {
// OTG mode is compatible with only very few options. // OTG mode is compatible with only very few options.
// Only report obvious errors. // Only report obvious errors.

View File

@ -87,7 +87,7 @@ write_position(uint8_t *buf, const struct sc_position *position) {
// write length (4 bytes) + string (non null-terminated) // write length (4 bytes) + string (non null-terminated)
static size_t static size_t
write_string(const char *utf8, size_t max_len, unsigned char *buf) { write_string(const char *utf8, size_t max_len, uint8_t *buf) {
size_t len = sc_str_utf8_truncation_index(utf8, max_len); size_t len = sc_str_utf8_truncation_index(utf8, max_len);
sc_write32be(buf, len); sc_write32be(buf, len);
memcpy(&buf[4], utf8, len); memcpy(&buf[4], utf8, len);
@ -95,7 +95,7 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
} }
size_t size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) { sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
buf[0] = msg->type; buf[0] = msg->type;
switch (msg->type) { switch (msg->type) {
case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE: case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE:
@ -146,10 +146,22 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
sc_write16be(&buf[1], msg->uhid_create.id);
sc_write16be(&buf[3], msg->uhid_create.report_desc_size);
memcpy(&buf[5], msg->uhid_create.report_desc,
msg->uhid_create.report_desc_size);
return 5 + msg->uhid_create.report_desc_size;
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
sc_write16be(&buf[1], msg->uhid_input.id);
sc_write16be(&buf[3], msg->uhid_input.size);
memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size);
return 5 + msg->uhid_input.size;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
// no additional data // no additional data
return 1; return 1;
default: default:
@ -242,6 +254,26 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
LOG_CMSG("rotate device"); LOG_CMSG("rotate device");
break; break;
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
LOG_CMSG("UHID create [%" PRIu16 "] report_desc_size=%" PRIu16,
msg->uhid_create.id, msg->uhid_create.report_desc_size);
break;
case SC_CONTROL_MSG_TYPE_UHID_INPUT: {
char *hex = sc_str_to_hex_string(msg->uhid_input.data,
msg->uhid_input.size);
if (hex) {
LOG_CMSG("UHID input [%" PRIu16 "] %s",
msg->uhid_input.id, hex);
free(hex);
} else {
LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16,
msg->uhid_input.id, msg->uhid_input.size);
}
break;
}
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
LOG_CMSG("open hard keyboard settings");
break;
default: default:
LOG_CMSG("unknown type: %u", (unsigned) msg->type); LOG_CMSG("unknown type: %u", (unsigned) msg->type);
break; break;

View File

@ -10,6 +10,7 @@
#include "android/input.h" #include "android/input.h"
#include "android/keycodes.h" #include "android/keycodes.h"
#include "coords.h" #include "coords.h"
#include "hid/hid_event.h"
#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
@ -37,6 +38,9 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
}; };
enum sc_screen_power_mode { enum sc_screen_power_mode {
@ -92,13 +96,23 @@ struct sc_control_msg {
struct { struct {
enum sc_screen_power_mode mode; enum sc_screen_power_mode mode;
} set_screen_power_mode; } set_screen_power_mode;
struct {
uint16_t id;
uint16_t report_desc_size;
const uint8_t *report_desc; // pointer to static data
} uhid_create;
struct {
uint16_t id;
uint16_t size;
uint8_t data[SC_HID_MAX_SIZE];
} uhid_input;
}; };
}; };
// buf size must be at least CONTROL_MSG_MAX_SIZE // buf size must be at least CONTROL_MSG_MAX_SIZE
// return the number of bytes written // return the number of bytes written
size_t size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf); sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf);
void void
sc_control_msg_log(const struct sc_control_msg *msg); sc_control_msg_log(const struct sc_control_msg *msg);

View File

@ -7,8 +7,7 @@
#define SC_CONTROL_MSG_QUEUE_MAX 64 #define SC_CONTROL_MSG_QUEUE_MAX 64
bool bool
sc_controller_init(struct sc_controller *controller, sc_socket control_socket, sc_controller_init(struct sc_controller *controller, sc_socket control_socket) {
struct sc_acksync *acksync) {
sc_vecdeque_init(&controller->queue); sc_vecdeque_init(&controller->queue);
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX); bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
@ -16,7 +15,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
return false; return false;
} }
ok = sc_receiver_init(&controller->receiver, control_socket, acksync); ok = sc_receiver_init(&controller->receiver, control_socket);
if (!ok) { if (!ok) {
sc_vecdeque_destroy(&controller->queue); sc_vecdeque_destroy(&controller->queue);
return false; return false;
@ -43,6 +42,14 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
return true; return true;
} }
void
sc_controller_configure(struct sc_controller *controller,
struct sc_acksync *acksync,
struct sc_uhid_devices *uhid_devices) {
controller->receiver.acksync = acksync;
controller->receiver.uhid_devices = uhid_devices;
}
void void
sc_controller_destroy(struct sc_controller *controller) { sc_controller_destroy(struct sc_controller *controller) {
sc_cond_destroy(&controller->msg_cond); sc_cond_destroy(&controller->msg_cond);
@ -84,7 +91,7 @@ sc_controller_push_msg(struct sc_controller *controller,
static bool static bool
process_msg(struct sc_controller *controller, process_msg(struct sc_controller *controller,
const struct sc_control_msg *msg) { const struct sc_control_msg *msg) {
static unsigned char serialized_msg[SC_CONTROL_MSG_MAX_SIZE]; static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
size_t length = sc_control_msg_serialize(msg, serialized_msg); size_t length = sc_control_msg_serialize(msg, serialized_msg);
if (!length) { if (!length) {
return false; return false;

View File

@ -25,8 +25,12 @@ struct sc_controller {
}; };
bool bool
sc_controller_init(struct sc_controller *controller, sc_socket control_socket, sc_controller_init(struct sc_controller *controller, sc_socket control_socket);
struct sc_acksync *acksync);
void
sc_controller_configure(struct sc_controller *controller,
struct sc_acksync *acksync,
struct sc_uhid_devices *uhid_devices);
void void
sc_controller_destroy(struct sc_controller *controller); sc_controller_destroy(struct sc_controller *controller);

View File

@ -8,19 +8,22 @@
#include "util/log.h" #include "util/log.h"
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, sc_device_msg_deserialize(const uint8_t *buf, size_t len,
struct device_msg *msg) { struct sc_device_msg *msg) {
if (len < 5) { if (!len) {
// at least type + empty string length return 0; // no message
return 0; // not available
} }
msg->type = buf[0]; msg->type = buf[0];
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
if (len < 5) {
// at least type + empty string length
return 0; // no complete message
}
size_t clipboard_len = sc_read32be(&buf[1]); size_t clipboard_len = sc_read32be(&buf[1]);
if (clipboard_len > len - 5) { if (clipboard_len > len - 5) {
return 0; // not available return 0; // no complete message
} }
char *text = malloc(clipboard_len + 1); char *text = malloc(clipboard_len + 1);
if (!text) { if (!text) {
@ -36,10 +39,38 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
return 5 + clipboard_len; return 5 + clipboard_len;
} }
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: { case DEVICE_MSG_TYPE_ACK_CLIPBOARD: {
if (len < 9) {
return 0; // no complete message
}
uint64_t sequence = sc_read64be(&buf[1]); uint64_t sequence = sc_read64be(&buf[1]);
msg->ack_clipboard.sequence = sequence; msg->ack_clipboard.sequence = sequence;
return 9; return 9;
} }
case DEVICE_MSG_TYPE_UHID_OUTPUT: {
if (len < 5) {
// at least id + size
return 0; // not available
}
uint16_t id = sc_read16be(&buf[1]);
size_t size = sc_read16be(&buf[3]);
if (size < len - 5) {
return 0; // not available
}
uint8_t *data = malloc(size);
if (!data) {
LOG_OOM();
return -1;
}
if (size) {
memcpy(data, &buf[5], size);
}
msg->uhid_output.id = id;
msg->uhid_output.size = size;
msg->uhid_output.data = data;
return 5 + size;
}
default: default:
LOGW("Unknown device message type: %d", (int) msg->type); LOGW("Unknown device message type: %d", (int) msg->type);
return -1; // error, we cannot recover return -1; // error, we cannot recover
@ -47,8 +78,16 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
} }
void void
device_msg_destroy(struct device_msg *msg) { sc_device_msg_destroy(struct sc_device_msg *msg) {
if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { switch (msg->type) {
free(msg->clipboard.text); case DEVICE_MSG_TYPE_CLIPBOARD:
free(msg->clipboard.text);
break;
case DEVICE_MSG_TYPE_UHID_OUTPUT:
free(msg->uhid_output.data);
break;
default:
// nothing to do
break;
} }
} }

View File

@ -11,13 +11,14 @@
// type: 1 byte; length: 4 bytes // type: 1 byte; length: 4 bytes
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
enum device_msg_type { enum sc_device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD,
DEVICE_MSG_TYPE_UHID_OUTPUT,
}; };
struct device_msg { struct sc_device_msg {
enum device_msg_type type; enum sc_device_msg_type type;
union { union {
struct { struct {
char *text; // owned, to be freed by free() char *text; // owned, to be freed by free()
@ -25,15 +26,20 @@ struct device_msg {
struct { struct {
uint64_t sequence; uint64_t sequence;
} ack_clipboard; } ack_clipboard;
struct {
uint16_t id;
uint16_t size;
uint8_t *data; // owned, to be freed by free()
} uhid_output;
}; };
}; };
// return the number of bytes consumed (0 for no msg available, -1 on error) // return the number of bytes consumed (0 for no msg available, -1 on error)
ssize_t ssize_t
device_msg_deserialize(const unsigned char *buf, size_t len, sc_device_msg_deserialize(const uint8_t *buf, size_t len,
struct device_msg *msg); struct sc_device_msg *msg);
void void
device_msg_destroy(struct device_msg *msg); sc_device_msg_destroy(struct sc_device_msg *msg);
#endif #endif

View File

@ -1,11 +1,40 @@
#include "display.h" #include "display.h"
#include <assert.h> #include <assert.h>
#include <libavutil/pixfmt.h>
#include "util/log.h" #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;
}
SDL_RenderClear(display->renderer);
if (display->texture) {
SDL_RenderCopy(display->renderer, display->texture, NULL, NULL);
}
SDL_RenderPresent(display->renderer);
return true;
}
bool 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 = display->renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!display->renderer) { if (!display->renderer) {
@ -59,11 +88,25 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
LOGI("Trilinear filtering disabled"); LOGI("Trilinear filtering disabled");
} }
} else if (mipmaps) { } else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer"); LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
} }
display->texture = NULL;
display->pending.flags = 0; display->pending.flags = 0;
display->pending.frame = NULL; 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; return true;
} }
@ -195,9 +238,25 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
return SC_DISPLAY_RESULT_OK; return SC_DISPLAY_RESULT_OK;
} }
static SDL_YUV_CONVERSION_MODE
sc_display_to_sdl_color_range(enum AVColorRange color_range) {
return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG
: SDL_YUV_CONVERSION_AUTOMATIC;
}
static bool static bool
sc_display_update_texture_internal(struct sc_display *display, sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) { const AVFrame *frame) {
if (!display->has_frame) {
// First frame
display->has_frame = true;
// Configure YUV color range conversion
SDL_YUV_CONVERSION_MODE sdl_color_range =
sc_display_to_sdl_color_range(frame->color_range);
SDL_SetYUVConversionMode(sdl_color_range);
}
int ret = SDL_UpdateYUVTexture(display->texture, NULL, int ret = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0], frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[1], frame->linesize[1],

View File

@ -33,6 +33,8 @@ struct sc_display {
struct sc_size size; struct sc_size size;
AVFrame *frame; AVFrame *frame;
} pending; } pending;
bool has_frame;
}; };
enum sc_display_result { enum sc_display_result {
@ -42,7 +44,8 @@ enum sc_display_result {
}; };
bool 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 void
sc_display_destroy(struct sc_display *display); sc_display_destroy(struct sc_display *display);

15
app/src/hid/hid_event.h Normal file
View File

@ -0,0 +1,15 @@
#ifndef SC_HID_EVENT_H
#define SC_HID_EVENT_H
#include "common.h"
#include <stdint.h>
#define SC_HID_MAX_SIZE 8
struct sc_hid_event {
uint8_t data[SC_HID_MAX_SIZE];
uint8_t size;
};
#endif

View File

@ -1,40 +1,34 @@
#include "hid_keyboard.h" #include "hid_keyboard.h"
#include <assert.h> #include <string.h>
#include "input_events.h"
#include "util/log.h" #include "util/log.h"
/** Downcast key processor to hid_keyboard */ #define SC_HID_MOD_NONE 0x00
#define DOWNCAST(KP) container_of(KP, struct sc_hid_keyboard, key_processor) #define SC_HID_MOD_LEFT_CONTROL (1 << 0)
#define SC_HID_MOD_LEFT_SHIFT (1 << 1)
#define SC_HID_MOD_LEFT_ALT (1 << 2)
#define SC_HID_MOD_LEFT_GUI (1 << 3)
#define SC_HID_MOD_RIGHT_CONTROL (1 << 4)
#define SC_HID_MOD_RIGHT_SHIFT (1 << 5)
#define SC_HID_MOD_RIGHT_ALT (1 << 6)
#define SC_HID_MOD_RIGHT_GUI (1 << 7)
#define HID_KEYBOARD_ACCESSORY_ID 1 #define SC_HID_KEYBOARD_INDEX_MODS 0
#define SC_HID_KEYBOARD_INDEX_KEYS 2
#define HID_MODIFIER_NONE 0x00
#define HID_MODIFIER_LEFT_CONTROL (1 << 0)
#define HID_MODIFIER_LEFT_SHIFT (1 << 1)
#define HID_MODIFIER_LEFT_ALT (1 << 2)
#define HID_MODIFIER_LEFT_GUI (1 << 3)
#define HID_MODIFIER_RIGHT_CONTROL (1 << 4)
#define HID_MODIFIER_RIGHT_SHIFT (1 << 5)
#define HID_MODIFIER_RIGHT_ALT (1 << 6)
#define HID_MODIFIER_RIGHT_GUI (1 << 7)
#define HID_KEYBOARD_INDEX_MODIFIER 0
#define HID_KEYBOARD_INDEX_KEYS 2
// USB HID protocol says 6 keys in an event is the requirement for BIOS // USB HID protocol says 6 keys in an event is the requirement for BIOS
// keyboard support, though OS could support more keys via modifying the report // keyboard support, though OS could support more keys via modifying the report
// desc. 6 should be enough for scrcpy. // desc. 6 should be enough for scrcpy.
#define HID_KEYBOARD_MAX_KEYS 6 #define SC_HID_KEYBOARD_MAX_KEYS 6
#define HID_KEYBOARD_EVENT_SIZE \ #define SC_HID_KEYBOARD_EVENT_SIZE \
(HID_KEYBOARD_INDEX_KEYS + HID_KEYBOARD_MAX_KEYS) (SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS)
#define HID_RESERVED 0x00 #define SC_HID_RESERVED 0x00
#define HID_ERROR_ROLL_OVER 0x01 #define SC_HID_ERROR_ROLL_OVER 0x01
/** /**
* For HID over AOAv2, only report descriptor is needed. * For HID, only report descriptor is needed.
* *
* The specification is available here: * The specification is available here:
* <https://www.usb.org/sites/default/files/hid1_11.pdf> * <https://www.usb.org/sites/default/files/hid1_11.pdf>
@ -53,7 +47,7 @@
* *
* (change vid:pid' to your device's vendor ID and product ID). * (change vid:pid' to your device's vendor ID and product ID).
*/ */
static const unsigned char keyboard_report_desc[] = { const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
// Usage Page (Generic Desktop) // Usage Page (Generic Desktop)
0x05, 0x01, 0x05, 0x01,
// Usage (Keyboard) // Usage (Keyboard)
@ -119,7 +113,7 @@ static const unsigned char keyboard_report_desc[] = {
// Report Size (8) // Report Size (8)
0x75, 0x08, 0x75, 0x08,
// Report Count (6) // Report Count (6)
0x95, HID_KEYBOARD_MAX_KEYS, 0x95, SC_HID_KEYBOARD_MAX_KEYS,
// Input (Data, Array): Keys // Input (Data, Array): Keys
0x81, 0x00, 0x81, 0x00,
@ -127,6 +121,9 @@ static const unsigned char keyboard_report_desc[] = {
0xC0 0xC0
}; };
const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN =
sizeof(SC_HID_KEYBOARD_REPORT_DESC);
/** /**
* A keyboard HID event is 8 bytes long: * A keyboard HID event is 8 bytes long:
* *
@ -201,51 +198,50 @@ static const unsigned char keyboard_report_desc[] = {
* +---------------+ * +---------------+
*/ */
static unsigned char static void
sdl_keymod_to_hid_modifiers(uint16_t mod) { sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) {
unsigned char modifiers = HID_MODIFIER_NONE; hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE;
if (mod & SC_MOD_LCTRL) {
modifiers |= HID_MODIFIER_LEFT_CONTROL; uint8_t *data = hid_event->data;
}
if (mod & SC_MOD_LSHIFT) { data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE;
modifiers |= HID_MODIFIER_LEFT_SHIFT; data[1] = SC_HID_RESERVED;
} memset(&data[SC_HID_KEYBOARD_INDEX_KEYS], 0, SC_HID_KEYBOARD_MAX_KEYS);
if (mod & SC_MOD_LALT) {
modifiers |= HID_MODIFIER_LEFT_ALT;
}
if (mod & SC_MOD_LGUI) {
modifiers |= HID_MODIFIER_LEFT_GUI;
}
if (mod & SC_MOD_RCTRL) {
modifiers |= HID_MODIFIER_RIGHT_CONTROL;
}
if (mod & SC_MOD_RSHIFT) {
modifiers |= HID_MODIFIER_RIGHT_SHIFT;
}
if (mod & SC_MOD_RALT) {
modifiers |= HID_MODIFIER_RIGHT_ALT;
}
if (mod & SC_MOD_RGUI) {
modifiers |= HID_MODIFIER_RIGHT_GUI;
}
return modifiers;
} }
static bool static uint16_t
sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) { sc_hid_mod_from_sdl_keymod(uint16_t mod) {
unsigned char *buffer = malloc(HID_KEYBOARD_EVENT_SIZE); uint16_t mods = SC_HID_MOD_NONE;
if (!buffer) { if (mod & SC_MOD_LCTRL) {
LOG_OOM(); mods |= SC_HID_MOD_LEFT_CONTROL;
return false;
} }
if (mod & SC_MOD_LSHIFT) {
mods |= SC_HID_MOD_LEFT_SHIFT;
}
if (mod & SC_MOD_LALT) {
mods |= SC_HID_MOD_LEFT_ALT;
}
if (mod & SC_MOD_LGUI) {
mods |= SC_HID_MOD_LEFT_GUI;
}
if (mod & SC_MOD_RCTRL) {
mods |= SC_HID_MOD_RIGHT_CONTROL;
}
if (mod & SC_MOD_RSHIFT) {
mods |= SC_HID_MOD_RIGHT_SHIFT;
}
if (mod & SC_MOD_RALT) {
mods |= SC_HID_MOD_RIGHT_ALT;
}
if (mod & SC_MOD_RGUI) {
mods |= SC_HID_MOD_RIGHT_GUI;
}
return mods;
}
buffer[HID_KEYBOARD_INDEX_MODIFIER] = HID_MODIFIER_NONE; void
buffer[1] = HID_RESERVED; sc_hid_keyboard_init(struct sc_hid_keyboard *hid) {
memset(&buffer[HID_KEYBOARD_INDEX_KEYS], 0, HID_KEYBOARD_MAX_KEYS); memset(hid->keys, false, SC_HID_KEYBOARD_KEYS);
sc_hid_event_init(hid_event, HID_KEYBOARD_ACCESSORY_ID, buffer,
HID_KEYBOARD_EVENT_SIZE);
return true;
} }
static inline bool static inline bool
@ -253,10 +249,10 @@ scancode_is_modifier(enum sc_scancode scancode) {
return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI; return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI;
} }
static bool bool
convert_hid_keyboard_event(struct sc_hid_keyboard *kb, sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid,
struct sc_hid_event *hid_event, struct sc_hid_event *hid_event,
const struct sc_key_event *event) { const struct sc_key_event *event) {
enum sc_scancode scancode = event->scancode; enum sc_scancode scancode = event->scancode;
assert(scancode >= 0); assert(scancode >= 0);
@ -268,39 +264,37 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb,
return false; return false;
} }
if (!sc_hid_keyboard_event_init(hid_event)) { sc_hid_keyboard_event_init(hid_event);
LOGW("Could not initialize HID keyboard event");
return false;
}
unsigned char modifiers = sdl_keymod_to_hid_modifiers(event->mods_state); uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state);
if (scancode < SC_HID_KEYBOARD_KEYS) { if (scancode < SC_HID_KEYBOARD_KEYS) {
// Pressed is true and released is false // Pressed is true and released is false
kb->keys[scancode] = (event->action == SC_ACTION_DOWN); hid->keys[scancode] = (event->action == SC_ACTION_DOWN);
LOGV("keys[%02x] = %s", scancode, LOGV("keys[%02x] = %s", scancode,
kb->keys[scancode] ? "true" : "false"); hid->keys[scancode] ? "true" : "false");
} }
hid_event->buffer[HID_KEYBOARD_INDEX_MODIFIER] = modifiers; hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods;
unsigned char *keys_buffer = &hid_event->buffer[HID_KEYBOARD_INDEX_KEYS]; uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS];
// Re-calculate pressed keys every time // Re-calculate pressed keys every time
int keys_pressed_count = 0; int keys_pressed_count = 0;
for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) { for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) {
if (kb->keys[i]) { if (hid->keys[i]) {
// USB HID protocol says that if keys exceeds report count, a // USB HID protocol says that if keys exceeds report count, a
// phantom state should be reported // phantom state should be reported
if (keys_pressed_count >= HID_KEYBOARD_MAX_KEYS) { if (keys_pressed_count >= SC_HID_KEYBOARD_MAX_KEYS) {
// Phantom state: // Phantom state:
// - Modifiers // - Modifiers
// - Reserved // - Reserved
// - ErrorRollOver * HID_MAX_KEYS // - ErrorRollOver * HID_MAX_KEYS
memset(keys_buffer, HID_ERROR_ROLL_OVER, HID_KEYBOARD_MAX_KEYS); memset(keys_data, SC_HID_ERROR_ROLL_OVER,
SC_HID_KEYBOARD_MAX_KEYS);
goto end; goto end;
} }
keys_buffer[keys_pressed_count] = i; keys_data[keys_pressed_count] = i;
++keys_pressed_count; ++keys_pressed_count;
} }
} }
@ -308,124 +302,32 @@ convert_hid_keyboard_event(struct sc_hid_keyboard *kb,
end: end:
LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x", LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x",
event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode, event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode,
event->scancode, modifiers); event->scancode, mods);
return true; return true;
} }
bool
static bool sc_hid_keyboard_event_from_mods(struct sc_hid_event *event,
push_mod_lock_state(struct sc_hid_keyboard *kb, uint16_t mods_state) { uint16_t mods_state) {
bool capslock = mods_state & SC_MOD_CAPS; bool capslock = mods_state & SC_MOD_CAPS;
bool numlock = mods_state & SC_MOD_NUM; bool numlock = mods_state & SC_MOD_NUM;
if (!capslock && !numlock) { if (!capslock && !numlock) {
// Nothing to do // Nothing to do
return true;
}
struct sc_hid_event hid_event;
if (!sc_hid_keyboard_event_init(&hid_event)) {
LOGW("Could not initialize HID keyboard event");
return false; return false;
} }
sc_hid_keyboard_event_init(event);
unsigned i = 0; unsigned i = 0;
if (capslock) { if (capslock) {
hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK; event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK;
++i; ++i;
} }
if (numlock) { if (numlock) {
hid_event.buffer[HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK; event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK;
++i; ++i;
} }
if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) {
sc_hid_event_destroy(&hid_event);
LOGW("Could not request HID event (mod lock state)");
return false;
}
LOGD("HID keyboard state synchronized");
return true; return true;
} }
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const struct sc_key_event *event,
uint64_t ack_to_wait) {
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
return;
}
struct sc_hid_keyboard *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
// Not all keys are supported, just ignore unsupported keys
if (convert_hid_keyboard_event(kb, &hid_event, event)) {
if (!kb->mod_lock_synchronized) {
// Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize
// keyboard state
if (push_mod_lock_state(kb, event->mods_state)) {
kb->mod_lock_synchronized = true;
}
}
if (ack_to_wait) {
// Ctrl+v is pressed, so clipboard synchronization has been
// requested. Wait until clipboard synchronization is acknowledged
// by the server, otherwise it could paste the old clipboard
// content.
hid_event.ack_to_wait = ack_to_wait;
}
if (!sc_aoa_push_hid_event(kb->aoa, &hid_event)) {
sc_hid_event_destroy(&hid_event);
LOGW("Could not request HID event (key)");
}
}
}
bool
sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa) {
kb->aoa = aoa;
bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID,
keyboard_report_desc,
ARRAY_LEN(keyboard_report_desc));
if (!ok) {
LOGW("Register HID keyboard failed");
return false;
}
// Reset all states
memset(kb->keys, false, SC_HID_KEYBOARD_KEYS);
kb->mod_lock_synchronized = false;
static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key,
// Never forward text input via HID (all the keys are injected
// separately)
.process_text = NULL,
};
// Clipboard synchronization is requested over the control socket, while HID
// events are sent over AOA, so it must wait for clipboard synchronization
// to be acknowledged by the device before injecting Ctrl+v.
kb->key_processor.async_paste = true;
kb->key_processor.ops = &ops;
return true;
}
void
sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb) {
// Unregister HID keyboard so the soft keyboard shows again on Android
bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID);
if (!ok) {
LOGW("Could not unregister HID keyboard");
}
}

View File

@ -5,8 +5,8 @@
#include <stdbool.h> #include <stdbool.h>
#include "aoa_hid.h" #include "hid/hid_event.h"
#include "trait/key_processor.h" #include "input_events.h"
// See "SDL2/SDL_scancode.h". // See "SDL2/SDL_scancode.h".
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB // Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
@ -14,6 +14,9 @@
// 0x65 is Application, typically AT-101 Keyboard ends here. // 0x65 is Application, typically AT-101 Keyboard ends here.
#define SC_HID_KEYBOARD_KEYS 0x66 #define SC_HID_KEYBOARD_KEYS 0x66
extern const uint8_t SC_HID_KEYBOARD_REPORT_DESC[];
extern const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN;
/** /**
* HID keyboard events are sequence-based, every time keyboard state changes * HID keyboard events are sequence-based, every time keyboard state changes
* it sends an array of currently pressed keys, the host is responsible for * it sends an array of currently pressed keys, the host is responsible for
@ -27,18 +30,19 @@
* phantom state. * phantom state.
*/ */
struct sc_hid_keyboard { struct sc_hid_keyboard {
struct sc_key_processor key_processor; // key processor trait
struct sc_aoa *aoa;
bool keys[SC_HID_KEYBOARD_KEYS]; bool keys[SC_HID_KEYBOARD_KEYS];
bool mod_lock_synchronized;
}; };
bool
sc_hid_keyboard_init(struct sc_hid_keyboard *kb, struct sc_aoa *aoa);
void void
sc_hid_keyboard_destroy(struct sc_hid_keyboard *kb); sc_hid_keyboard_init(struct sc_hid_keyboard *hid);
bool
sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid,
struct sc_hid_event *hid_event,
const struct sc_key_event *event);
bool
sc_hid_keyboard_event_from_mods(struct sc_hid_event *event,
uint16_t mods_state);
#endif #endif

192
app/src/hid/hid_mouse.c Normal file
View File

@ -0,0 +1,192 @@
#include "hid_mouse.h"
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position,
// 1 byte for wheel motion
#define HID_MOUSE_EVENT_SIZE 4
/**
* Mouse descriptor from the specification:
* <https://www.usb.org/sites/default/files/hid1_11.pdf>
*
* Appendix E (p71): §E.10 Report Descriptor (Mouse)
*
* The usage tags (like Wheel) are listed in "HID Usage Tables":
* <https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf>
* §4 Generic Desktop Page (0x01) (p26)
*/
const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Mouse)
0x09, 0x02,
// Collection (Application)
0xA1, 0x01,
// Usage (Pointer)
0x09, 0x01,
// Collection (Physical)
0xA1, 0x00,
// Usage Page (Buttons)
0x05, 0x09,
// Usage Minimum (1)
0x19, 0x01,
// Usage Maximum (5)
0x29, 0x05,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (1)
0x25, 0x01,
// Report Count (5)
0x95, 0x05,
// Report Size (1)
0x75, 0x01,
// Input (Data, Variable, Absolute): 5 buttons bits
0x81, 0x02,
// Report Count (1)
0x95, 0x01,
// Report Size (3)
0x75, 0x03,
// Input (Constant): 3 bits padding
0x81, 0x01,
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (X)
0x09, 0x30,
// Usage (Y)
0x09, 0x31,
// Usage (Wheel)
0x09, 0x38,
// Local Minimum (-127)
0x15, 0x81,
// Local Maximum (127)
0x25, 0x7F,
// Report Size (8)
0x75, 0x08,
// Report Count (3)
0x95, 0x03,
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06,
// End Collection
0xC0,
// End Collection
0xC0,
};
const size_t SC_HID_MOUSE_REPORT_DESC_LEN =
sizeof(SC_HID_MOUSE_REPORT_DESC);
/**
* A mouse HID event is 4 bytes long:
*
* - byte 0: buttons state
* - byte 1: relative x motion (signed byte from -127 to 127)
* - byte 2: relative y motion (signed byte from -127 to 127)
* - byte 3: wheel motion (-1, 0 or 1)
*
* 7 6 5 4 3 2 1 0
* +---------------+
* byte 0: |0 0 0 . . . . .| buttons state
* +---------------+
* ^ ^ ^ ^ ^
* | | | | `- left button
* | | | `--- right button
* | | `----- middle button
* | `------- button 4
* `--------- button 5
*
* +---------------+
* byte 1: |. . . . . . . .| relative x motion
* +---------------+
* byte 2: |. . . . . . . .| relative y motion
* +---------------+
* byte 3: |. . . . . . . .| wheel motion
* +---------------+
*
* As an example, here is the report for a motion of (x=5, y=-4) with left
* button pressed:
*
* +---------------+
* |0 0 0 0 0 0 0 1| left button pressed
* +---------------+
* |0 0 0 0 0 1 0 1| horizontal motion (x = 5)
* +---------------+
* |1 1 1 1 1 1 0 0| relative y motion (y = -4)
* +---------------+
* |0 0 0 0 0 0 0 0| wheel motion
* +---------------+
*/
static void
sc_hid_mouse_event_init(struct sc_hid_event *hid_event) {
hid_event->size = HID_MOUSE_EVENT_SIZE;
// Leave hid_event->data uninitialized, it will be fully initialized by
// callers
}
static uint8_t
sc_hid_buttons_from_buttons_state(uint8_t buttons_state) {
uint8_t c = 0;
if (buttons_state & SC_MOUSE_BUTTON_LEFT) {
c |= 1 << 0;
}
if (buttons_state & SC_MOUSE_BUTTON_RIGHT) {
c |= 1 << 1;
}
if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) {
c |= 1 << 2;
}
if (buttons_state & SC_MOUSE_BUTTON_X1) {
c |= 1 << 3;
}
if (buttons_state & SC_MOUSE_BUTTON_X2) {
c |= 1 << 4;
}
return c;
}
void
sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event,
const struct sc_mouse_motion_event *event) {
sc_hid_mouse_event_init(hid_event);
uint8_t *data = hid_event->data;
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = CLAMP(event->xrel, -127, 127);
data[2] = CLAMP(event->yrel, -127, 127);
data[3] = 0; // wheel coordinates only used for scrolling
}
void
sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event,
const struct sc_mouse_click_event *event) {
sc_hid_mouse_event_init(hid_event);
uint8_t *data = hid_event->data;
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion
data[2] = 0; // no y motion
data[3] = 0; // wheel coordinates only used for scrolling
}
void
sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event,
const struct sc_mouse_scroll_event *event) {
sc_hid_mouse_event_init(hid_event);
uint8_t *data = hid_event->data;
data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion
data[2] = 0; // no y motion
// In practice, vscroll is always -1, 0 or 1, but in theory other values
// are possible
data[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
}

26
app/src/hid/hid_mouse.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef SC_HID_MOUSE_H
#define SC_HID_MOUSE_H
#endif
#include "common.h"
#include <stdbool.h>
#include "hid/hid_event.h"
#include "input_events.h"
extern const uint8_t SC_HID_MOUSE_REPORT_DESC[];
extern const size_t SC_HID_MOUSE_REPORT_DESC_LEN;
void
sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event,
const struct sc_mouse_motion_event *event);
void
sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event,
const struct sc_mouse_click_event *event);
void
sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event,
const struct sc_mouse_scroll_event *event);

View File

@ -52,8 +52,11 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
void void
sc_input_manager_init(struct sc_input_manager *im, sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) { const struct sc_input_manager_params *params) {
assert(!params->controller || (params->kp && params->kp->ops)); // A key/mouse processor may not be present if there is no controller
assert(!params->controller || (params->mp && params->mp->ops)); assert((!params->kp && !params->mp) || params->controller);
// A processor must have ops initialized
assert(!params->kp || params->kp->ops);
assert(!params->mp || params->mp->ops);
im->controller = params->controller; im->controller = params->controller;
im->fp = params->fp; im->fp = params->fp;
@ -76,6 +79,8 @@ sc_input_manager_init(struct sc_input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count; im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false; im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->last_keycode = SDLK_UNKNOWN; im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0; im->last_mod = 0;
@ -85,8 +90,10 @@ sc_input_manager_init(struct sc_input_manager *im,
} }
static void static void
send_keycode(struct sc_controller *controller, enum android_keycode keycode, send_keycode(struct sc_input_manager *im, enum android_keycode keycode,
enum sc_action action, const char *name) { enum sc_action action, const char *name) {
assert(im->controller && im->kp);
// send DOWN event // send DOWN event
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE; msg.type = SC_CONTROL_MSG_TYPE_INJECT_KEYCODE;
@ -97,100 +104,109 @@ send_keycode(struct sc_controller *controller, enum android_keycode keycode,
msg.inject_keycode.metastate = 0; msg.inject_keycode.metastate = 0;
msg.inject_keycode.repeat = 0; msg.inject_keycode.repeat = 0;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'inject %s'", name); LOGW("Could not request 'inject %s'", name);
} }
} }
static inline void static inline void
action_home(struct sc_controller *controller, enum sc_action action) { action_home(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_HOME, action, "HOME"); send_keycode(im, AKEYCODE_HOME, action, "HOME");
} }
static inline void static inline void
action_back(struct sc_controller *controller, enum sc_action action) { action_back(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_BACK, action, "BACK"); send_keycode(im, AKEYCODE_BACK, action, "BACK");
} }
static inline void static inline void
action_app_switch(struct sc_controller *controller, enum sc_action action) { action_app_switch(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_APP_SWITCH, action, "APP_SWITCH"); send_keycode(im, AKEYCODE_APP_SWITCH, action, "APP_SWITCH");
} }
static inline void static inline void
action_power(struct sc_controller *controller, enum sc_action action) { action_power(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_POWER, action, "POWER"); send_keycode(im, AKEYCODE_POWER, action, "POWER");
} }
static inline void static inline void
action_volume_up(struct sc_controller *controller, enum sc_action action) { action_volume_up(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_VOLUME_UP, action, "VOLUME_UP"); send_keycode(im, AKEYCODE_VOLUME_UP, action, "VOLUME_UP");
} }
static inline void static inline void
action_volume_down(struct sc_controller *controller, enum sc_action action) { action_volume_down(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN"); send_keycode(im, AKEYCODE_VOLUME_DOWN, action, "VOLUME_DOWN");
} }
static inline void static inline void
action_menu(struct sc_controller *controller, enum sc_action action) { action_menu(struct sc_input_manager *im, enum sc_action action) {
send_keycode(controller, AKEYCODE_MENU, action, "MENU"); send_keycode(im, AKEYCODE_MENU, action, "MENU");
} }
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
// If the screen is off, it is turned on only on ACTION_DOWN // If the screen is off, it is turned on only on ACTION_DOWN
static void static void
press_back_or_turn_screen_on(struct sc_controller *controller, press_back_or_turn_screen_on(struct sc_input_manager *im,
enum sc_action action) { enum sc_action action) {
assert(im->controller && im->kp);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
msg.back_or_screen_on.action = action == SC_ACTION_DOWN msg.back_or_screen_on.action = action == SC_ACTION_DOWN
? AKEY_EVENT_ACTION_DOWN ? AKEY_EVENT_ACTION_DOWN
: AKEY_EVENT_ACTION_UP; : AKEY_EVENT_ACTION_UP;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'"); LOGW("Could not request 'press back or turn screen on'");
} }
} }
static void static void
expand_notification_panel(struct sc_controller *controller) { expand_notification_panel(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'expand notification panel'"); LOGW("Could not request 'expand notification panel'");
} }
} }
static void static void
expand_settings_panel(struct sc_controller *controller) { expand_settings_panel(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL; msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'expand settings panel'"); LOGW("Could not request 'expand settings panel'");
} }
} }
static void static void
collapse_panels(struct sc_controller *controller) { collapse_panels(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS; msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'collapse notification panel'"); LOGW("Could not request 'collapse notification panel'");
} }
} }
static bool static bool
get_device_clipboard(struct sc_controller *controller, get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
enum sc_copy_key copy_key) { assert(im->controller && im->kp);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD; msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD;
msg.get_clipboard.copy_key = copy_key; msg.get_clipboard.copy_key = copy_key;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'get device clipboard'"); LOGW("Could not request 'get device clipboard'");
return false; return false;
} }
@ -199,8 +215,10 @@ get_device_clipboard(struct sc_controller *controller,
} }
static bool static bool
set_device_clipboard(struct sc_controller *controller, bool paste, set_device_clipboard(struct sc_input_manager *im, bool paste,
uint64_t sequence) { uint64_t sequence) {
assert(im->controller && im->kp);
char *text = SDL_GetClipboardText(); char *text = SDL_GetClipboardText();
if (!text) { if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError()); LOGW("Could not get clipboard text: %s", SDL_GetError());
@ -220,7 +238,7 @@ set_device_clipboard(struct sc_controller *controller, bool paste,
msg.set_clipboard.text = text_dup; msg.set_clipboard.text = text_dup;
msg.set_clipboard.paste = paste; msg.set_clipboard.paste = paste;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
free(text_dup); free(text_dup);
LOGW("Could not request 'set device clipboard'"); LOGW("Could not request 'set device clipboard'");
return false; return false;
@ -230,19 +248,23 @@ set_device_clipboard(struct sc_controller *controller, bool paste,
} }
static void static void
set_screen_power_mode(struct sc_controller *controller, set_screen_power_mode(struct sc_input_manager *im,
enum sc_screen_power_mode mode) { enum sc_screen_power_mode mode) {
assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = mode; msg.set_screen_power_mode.mode = mode;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set screen power mode'");
} }
} }
static void static void
switch_fps_counter_state(struct sc_fps_counter *fps_counter) { switch_fps_counter_state(struct sc_input_manager *im) {
struct sc_fps_counter *fps_counter = &im->screen->fps_counter;
// the started state can only be written from the current thread, so there // the started state can only be written from the current thread, so there
// is no ToCToU issue // is no ToCToU issue
if (sc_fps_counter_is_started(fps_counter)) { if (sc_fps_counter_is_started(fps_counter)) {
@ -254,7 +276,9 @@ switch_fps_counter_state(struct sc_fps_counter *fps_counter) {
} }
static void static void
clipboard_paste(struct sc_controller *controller) { clipboard_paste(struct sc_input_manager *im) {
assert(im->controller && im->kp);
char *text = SDL_GetClipboardText(); char *text = SDL_GetClipboardText();
if (!text) { if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError()); LOGW("Could not get clipboard text: %s", SDL_GetError());
@ -276,25 +300,40 @@ clipboard_paste(struct sc_controller *controller) {
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT; msg.type = SC_CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = text_dup; msg.inject_text.text = text_dup;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
free(text_dup); free(text_dup);
LOGW("Could not request 'paste clipboard'"); LOGW("Could not request 'paste clipboard'");
} }
} }
static void static void
rotate_device(struct sc_controller *controller) { rotate_device(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg; struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE; msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE;
if (!sc_controller_push_msg(controller, &msg)) { if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request device rotation"); LOGW("Could not request device rotation");
} }
} }
static void static void
apply_orientation_transform(struct sc_screen *screen, open_hard_keyboard_settings(struct sc_input_manager *im) {
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS;
if (!sc_controller_push_msg(im->controller, &msg)) {
LOGW("Could not request opening hard keyboard settings");
}
}
static void
apply_orientation_transform(struct sc_input_manager *im,
enum sc_orientation transform) { enum sc_orientation transform) {
struct sc_screen *screen = im->screen;
enum sc_orientation new_orientation = enum sc_orientation new_orientation =
sc_orientation_apply(screen->orientation, transform); sc_orientation_apply(screen->orientation, transform);
sc_screen_set_orientation(screen, new_orientation); sc_screen_set_orientation(screen, new_orientation);
@ -347,9 +386,14 @@ simulate_virtual_finger(struct sc_input_manager *im,
} }
static struct sc_point static struct sc_point
inverse_point(struct sc_point point, struct sc_size size) { inverse_point(struct sc_point point, struct sc_size size,
point.x = size.width - point.x; bool invert_x, bool invert_y) {
point.y = size.height - point.y; if (invert_x) {
point.x = size.width - point.x;
}
if (invert_y) {
point.y = size.height - point.y;
}
return point; return point;
} }
@ -357,7 +401,9 @@ static void
sc_input_manager_process_key(struct sc_input_manager *im, sc_input_manager_process_key(struct sc_input_manager *im,
const SDL_KeyboardEvent *event) { const SDL_KeyboardEvent *event) {
// controller is NULL if --no-control is requested // controller is NULL if --no-control is requested
struct sc_controller *controller = im->controller; bool control = im->controller;
bool paused = im->screen->paused;
bool video = im->screen->video;
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod; uint16_t mod = event->keysym.mod;
@ -383,140 +429,151 @@ sc_input_manager_process_key(struct sc_input_manager *im,
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
switch (keycode) { switch (keycode) {
case SDLK_h: case SDLK_h:
if (controller && !shift && !repeat) { if (im->kp && !shift && !repeat && !paused) {
action_home(controller, action); action_home(im, action);
} }
return; return;
case SDLK_b: // fall-through case SDLK_b: // fall-through
case SDLK_BACKSPACE: case SDLK_BACKSPACE:
if (controller && !shift && !repeat) { if (im->kp && !shift && !repeat && !paused) {
action_back(controller, action); action_back(im, action);
} }
return; return;
case SDLK_s: case SDLK_s:
if (controller && !shift && !repeat) { if (im->kp && !shift && !repeat && !paused) {
action_app_switch(controller, action); action_app_switch(im, action);
} }
return; return;
case SDLK_m: case SDLK_m:
if (controller && !shift && !repeat) { if (im->kp && !shift && !repeat && !paused) {
action_menu(controller, action); action_menu(im, action);
} }
return; return;
case SDLK_p: case SDLK_p:
if (controller && !shift && !repeat) { if (im->kp && !shift && !repeat && !paused) {
action_power(controller, action); action_power(im, action);
} }
return; return;
case SDLK_o: case SDLK_o:
if (controller && !repeat && down) { if (control && !repeat && down && !paused) {
enum sc_screen_power_mode mode = shift enum sc_screen_power_mode mode = shift
? SC_SCREEN_POWER_MODE_NORMAL ? SC_SCREEN_POWER_MODE_NORMAL
: SC_SCREEN_POWER_MODE_OFF; : SC_SCREEN_POWER_MODE_OFF;
set_screen_power_mode(controller, mode); set_screen_power_mode(im, mode);
}
return;
case SDLK_z:
if (video && down && !repeat) {
sc_screen_set_paused(im->screen, !shift);
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (shift) { if (shift) {
if (!repeat & down) { if (video && !repeat && down) {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180); SC_ORIENTATION_FLIP_180);
} }
} else if (controller) { } else if (im->kp && !paused) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(im, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (shift) { if (shift) {
if (!repeat & down) { if (video && !repeat && down) {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_FLIP_180); SC_ORIENTATION_FLIP_180);
} }
} else if (controller) { } else if (im->kp && !paused) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(im, action);
} }
return; return;
case SDLK_LEFT: case SDLK_LEFT:
if (!repeat && down) { if (video && !repeat && down) {
if (shift) { if (shift) {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0); SC_ORIENTATION_FLIP_0);
} else { } else {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_270); SC_ORIENTATION_270);
} }
} }
return; return;
case SDLK_RIGHT: case SDLK_RIGHT:
if (!repeat && down) { if (video && !repeat && down) {
if (shift) { if (shift) {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_FLIP_0); SC_ORIENTATION_FLIP_0);
} else { } else {
apply_orientation_transform(im->screen, apply_orientation_transform(im,
SC_ORIENTATION_90); SC_ORIENTATION_90);
} }
} }
return; return;
case SDLK_c: case SDLK_c:
if (controller && !shift && !repeat && down) { if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(controller, SC_COPY_KEY_COPY); get_device_clipboard(im, SC_COPY_KEY_COPY);
} }
return; return;
case SDLK_x: case SDLK_x:
if (controller && !shift && !repeat && down) { if (im->kp && !shift && !repeat && down && !paused) {
get_device_clipboard(controller, SC_COPY_KEY_CUT); get_device_clipboard(im, SC_COPY_KEY_CUT);
} }
return; return;
case SDLK_v: case SDLK_v:
if (controller && !repeat && down) { if (im->kp && !repeat && down && !paused) {
if (shift || im->legacy_paste) { if (shift || im->legacy_paste) {
// inject the text as input events // inject the text as input events
clipboard_paste(controller); clipboard_paste(im);
} else { } else {
// store the text in the device clipboard and paste, // store the text in the device clipboard and paste,
// without requesting an acknowledgment // without requesting an acknowledgment
set_device_clipboard(controller, true, set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
SC_SEQUENCE_INVALID);
} }
} }
return; return;
case SDLK_f: case SDLK_f:
if (!shift && !repeat && down) { if (video && !shift && !repeat && down) {
sc_screen_switch_fullscreen(im->screen); sc_screen_switch_fullscreen(im->screen);
} }
return; return;
case SDLK_w: case SDLK_w:
if (!shift && !repeat && down) { if (video && !shift && !repeat && down) {
sc_screen_resize_to_fit(im->screen); sc_screen_resize_to_fit(im->screen);
} }
return; return;
case SDLK_g: case SDLK_g:
if (!shift && !repeat && down) { if (video && !shift && !repeat && down) {
sc_screen_resize_to_pixel_perfect(im->screen); sc_screen_resize_to_pixel_perfect(im->screen);
} }
return; return;
case SDLK_i: case SDLK_i:
if (!shift && !repeat && down) { if (video && !shift && !repeat && down) {
switch_fps_counter_state(&im->screen->fps_counter); switch_fps_counter_state(im);
} }
return; return;
case SDLK_n: case SDLK_n:
if (controller && !repeat && down) { if (control && !repeat && down && !paused) {
if (shift) { if (shift) {
collapse_panels(controller); collapse_panels(im);
} else if (im->key_repeat == 0) { } else if (im->key_repeat == 0) {
expand_notification_panel(controller); expand_notification_panel(im);
} else { } else {
expand_settings_panel(controller); expand_settings_panel(im);
} }
} }
return; return;
case SDLK_r: case SDLK_r:
if (controller && !shift && !repeat && down) { if (control && !shift && !repeat && down && !paused) {
rotate_device(controller); rotate_device(im);
}
return;
case SDLK_k:
if (control && !shift && !repeat && down && !paused
&& im->kp && im->kp->hid) {
// Only if the current keyboard is hid
open_hard_keyboard_settings(im);
} }
return; return;
} }
@ -524,7 +581,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
return; return;
} }
if (!controller) { if (!im->kp || paused) {
return; return;
} }
@ -533,7 +590,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
if (im->clipboard_autosync && is_ctrl_v) { if (im->clipboard_autosync && is_ctrl_v) {
if (im->legacy_paste) { if (im->legacy_paste) {
// inject the text as input events // inject the text as input events
clipboard_paste(controller); clipboard_paste(im);
return; return;
} }
@ -543,7 +600,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
// Synchronize the computer clipboard to the device clipboard before // Synchronize the computer clipboard to the device clipboard before
// sending Ctrl+v, to allow seamless copy-paste. // sending Ctrl+v, to allow seamless copy-paste.
bool ok = set_device_clipboard(controller, false, sequence); bool ok = set_device_clipboard(im, false, sequence);
if (!ok) { if (!ok) {
LOGW("Clipboard could not be synchronized, Ctrl+v not injected"); LOGW("Clipboard could not be synchronized, Ctrl+v not injected");
return; return;
@ -569,22 +626,33 @@ sc_input_manager_process_key(struct sc_input_manager *im,
im->kp->ops->process_key(im->kp, &evt, ack_to_wait); 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 static void
sc_input_manager_process_mouse_motion(struct sc_input_manager *im, sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
const SDL_MouseMotionEvent *event) { const SDL_MouseMotionEvent *event) {
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
} }
struct sc_mouse_motion_event evt = { struct sc_mouse_motion_event evt = {
.position = { .position = sc_input_manager_get_position(im, event->x, event->y),
.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 = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER, : POINTER_ID_GENERIC_FINGER,
.xrel = event->xrel, .xrel = event->xrel,
@ -605,7 +673,9 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
im->vfinger_invert_x,
im->vfinger_invert_y);
simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger); simulate_virtual_finger(im, AMOTION_EVENT_ACTION_MOVE, vfinger);
} }
} }
@ -643,36 +713,36 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
static void static void
sc_input_manager_process_mouse_button(struct sc_input_manager *im, sc_input_manager_process_mouse_button(struct sc_input_manager *im,
const SDL_MouseButtonEvent *event) { const SDL_MouseButtonEvent *event) {
struct sc_controller *controller = im->controller;
if (event->which == SDL_TOUCH_MOUSEID) { if (event->which == SDL_TOUCH_MOUSEID) {
// simulated from touch events, so it's a duplicate // simulated from touch events, so it's a duplicate
return; return;
} }
bool control = im->controller;
bool paused = im->screen->paused;
bool down = event->type == SDL_MOUSEBUTTONDOWN; bool down = event->type == SDL_MOUSEBUTTONDOWN;
if (!im->forward_all_clicks) { if (!im->forward_all_clicks) {
if (controller) { if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP; enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
if (event->button == SDL_BUTTON_X1) { if (im->kp && event->button == SDL_BUTTON_X1) {
action_app_switch(controller, action); action_app_switch(im, action);
return; return;
} }
if (event->button == SDL_BUTTON_X2 && down) { if (event->button == SDL_BUTTON_X2 && down) {
if (event->clicks < 2) { if (event->clicks < 2) {
expand_notification_panel(controller); expand_notification_panel(im);
} else { } else {
expand_settings_panel(controller); expand_settings_panel(im);
} }
return; return;
} }
if (event->button == SDL_BUTTON_RIGHT) { if (im->kp && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(controller, action); press_back_or_turn_screen_on(im, action);
return; return;
} }
if (event->button == SDL_BUTTON_MIDDLE) { if (im->kp && event->button == SDL_BUTTON_MIDDLE) {
action_home(controller, action); action_home(im, action);
return; return;
} }
} }
@ -695,19 +765,14 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// otherwise, send the click event to the device // otherwise, send the click event to the device
} }
if (!controller) { if (!im->mp || paused) {
return; return;
} }
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL); uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
struct sc_mouse_click_event evt = { struct sc_mouse_click_event evt = {
.position = { .position = sc_input_manager_get_position(im, event->x, event->y),
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
event->x,
event->y),
},
.action = sc_action_from_sdl_mousebutton_type(event->type), .action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button), .button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE .pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
@ -726,7 +791,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
return; return;
} }
// Pinch-to-zoom simulation. // Pinch-to-zoom, rotate and tilt simulation.
// //
// If Ctrl is hold when the left-click button is pressed, then // If Ctrl is hold when the left-click button is pressed, then
// pinch-to-zoom mode is enabled: on every mouse event until the left-click // pinch-to-zoom mode is enabled: on every mouse event until the left-click
@ -735,14 +800,29 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// //
// In other words, the center of the rotation/scaling is the center of the // In other words, the center of the rotation/scaling is the center of the
// screen. // screen.
#define CTRL_PRESSED (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) //
// To simulate a tilt gesture (a vertical slide with two fingers), Shift
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
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 && if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && CTRL_PRESSED) || ((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) { (!down && im->vfinger_down))) {
struct sc_point mouse = struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x, sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y); event->y);
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size); if (down) {
im->vfinger_invert_x = ctrl_pressed || shift_pressed;
im->vfinger_invert_y = ctrl_pressed;
}
struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size,
im->vfinger_invert_x,
im->vfinger_invert_y);
enum android_motionevent_action action = down enum android_motionevent_action action = down
? AMOTION_EVENT_ACTION_DOWN ? AMOTION_EVENT_ACTION_DOWN
: AMOTION_EVENT_ACTION_UP; : AMOTION_EVENT_ACTION_UP;
@ -767,11 +847,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y); uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
struct sc_mouse_scroll_event evt = { struct sc_mouse_scroll_event evt = {
.position = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y),
.screen_size = im->screen->frame_size,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
mouse_x, mouse_y),
},
#if SDL_VERSION_ATLEAST(2, 0, 18) #if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
@ -818,9 +894,10 @@ void
sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event) { const SDL_Event *event) {
bool control = im->controller; bool control = im->controller;
bool paused = im->screen->paused;
switch (event->type) { switch (event->type) {
case SDL_TEXTINPUT: case SDL_TEXTINPUT:
if (!control) { if (!im->kp || paused) {
break; break;
} }
sc_input_manager_process_text_input(im, &event->text); sc_input_manager_process_text_input(im, &event->text);
@ -832,13 +909,13 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
sc_input_manager_process_key(im, &event->key); sc_input_manager_process_key(im, &event->key);
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
if (!control) { if (!im->mp || paused) {
break; break;
} }
sc_input_manager_process_mouse_motion(im, &event->motion); sc_input_manager_process_mouse_motion(im, &event->motion);
break; break;
case SDL_MOUSEWHEEL: case SDL_MOUSEWHEEL:
if (!control) { if (!im->mp || paused) {
break; break;
} }
sc_input_manager_process_mouse_wheel(im, &event->wheel); sc_input_manager_process_mouse_wheel(im, &event->wheel);
@ -852,7 +929,7 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
case SDL_FINGERMOTION: case SDL_FINGERMOTION:
case SDL_FINGERDOWN: case SDL_FINGERDOWN:
case SDL_FINGERUP: case SDL_FINGERUP:
if (!control) { if (!im->mp || paused) {
break; break;
} }
sc_input_manager_process_touch(im, &event->tfinger); sc_input_manager_process_touch(im, &event->tfinger);

View File

@ -32,6 +32,8 @@ struct sc_input_manager {
} sdl_shortcut_mods; } sdl_shortcut_mods;
bool vfinger_down; bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
// Tracks the number of identical consecutive shortcut key down events. // Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of // Not to be confused with event->repeat, which counts the number of

View File

@ -1,4 +1,4 @@
#include "keyboard_inject.h" #include "keyboard_sdk.h"
#include <assert.h> #include <assert.h>
@ -9,8 +9,8 @@
#include "util/intmap.h" #include "util/intmap.h"
#include "util/log.h" #include "util/log.h"
/** Downcast key processor to sc_keyboard_inject */ /** Downcast key processor to sc_keyboard_sdk */
#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_inject, key_processor) #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_sdk, key_processor)
static enum android_keyevent_action static enum android_keyevent_action
convert_keycode_action(enum sc_action action) { convert_keycode_action(enum sc_action action) {
@ -271,20 +271,20 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
// is set before injecting Ctrl+v. // is set before injecting Ctrl+v.
(void) ack_to_wait; (void) ack_to_wait;
struct sc_keyboard_inject *ki = DOWNCAST(kp); struct sc_keyboard_sdk *kb = DOWNCAST(kp);
if (event->repeat) { if (event->repeat) {
if (!ki->forward_key_repeat) { if (!kb->forward_key_repeat) {
return; return;
} }
++ki->repeat; ++kb->repeat;
} else { } else {
ki->repeat = 0; kb->repeat = 0;
} }
struct sc_control_msg msg; struct sc_control_msg msg;
if (convert_input_key(event, &msg, ki->key_inject_mode, ki->repeat)) { if (convert_input_key(event, &msg, kb->key_inject_mode, kb->repeat)) {
if (!sc_controller_push_msg(ki->controller, &msg)) { if (!sc_controller_push_msg(kb->controller, &msg)) {
LOGW("Could not request 'inject keycode'"); LOGW("Could not request 'inject keycode'");
} }
} }
@ -293,14 +293,14 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
static void static void
sc_key_processor_process_text(struct sc_key_processor *kp, sc_key_processor_process_text(struct sc_key_processor *kp,
const struct sc_text_event *event) { const struct sc_text_event *event) {
struct sc_keyboard_inject *ki = DOWNCAST(kp); struct sc_keyboard_sdk *kb = DOWNCAST(kp);
if (ki->key_inject_mode == SC_KEY_INJECT_MODE_RAW) { if (kb->key_inject_mode == SC_KEY_INJECT_MODE_RAW) {
// Never inject text events // Never inject text events
return; return;
} }
if (ki->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) { if (kb->key_inject_mode == SC_KEY_INJECT_MODE_MIXED) {
char c = event->text[0]; char c = event->text[0];
if (isalpha(c) || c == ' ') { if (isalpha(c) || c == ' ') {
assert(event->text[1] == '\0'); assert(event->text[1] == '\0');
@ -316,22 +316,22 @@ sc_key_processor_process_text(struct sc_key_processor *kp,
LOGW("Could not strdup input text"); LOGW("Could not strdup input text");
return; return;
} }
if (!sc_controller_push_msg(ki->controller, &msg)) { if (!sc_controller_push_msg(kb->controller, &msg)) {
free(msg.inject_text.text); free(msg.inject_text.text);
LOGW("Could not request 'inject text'"); LOGW("Could not request 'inject text'");
} }
} }
void void
sc_keyboard_inject_init(struct sc_keyboard_inject *ki, sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb,
struct sc_controller *controller, struct sc_controller *controller,
enum sc_key_inject_mode key_inject_mode, enum sc_key_inject_mode key_inject_mode,
bool forward_key_repeat) { bool forward_key_repeat) {
ki->controller = controller; kb->controller = controller;
ki->key_inject_mode = key_inject_mode; kb->key_inject_mode = key_inject_mode;
ki->forward_key_repeat = forward_key_repeat; kb->forward_key_repeat = forward_key_repeat;
ki->repeat = 0; kb->repeat = 0;
static const struct sc_key_processor_ops ops = { static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key, .process_key = sc_key_processor_process_key,
@ -339,6 +339,7 @@ sc_keyboard_inject_init(struct sc_keyboard_inject *ki,
}; };
// Key injection and clipboard synchronization are serialized // Key injection and clipboard synchronization are serialized
ki->key_processor.async_paste = false; kb->key_processor.async_paste = false;
ki->key_processor.ops = &ops; kb->key_processor.hid = false;
kb->key_processor.ops = &ops;
} }

View File

@ -1,5 +1,5 @@
#ifndef SC_KEYBOARD_INJECT_H #ifndef SC_KEYBOARD_SDK_H
#define SC_KEYBOARD_INJECT_H #define SC_KEYBOARD_SDK_H
#include "common.h" #include "common.h"
@ -9,7 +9,7 @@
#include "options.h" #include "options.h"
#include "trait/key_processor.h" #include "trait/key_processor.h"
struct sc_keyboard_inject { struct sc_keyboard_sdk {
struct sc_key_processor key_processor; // key processor trait struct sc_key_processor key_processor; // key processor trait
struct sc_controller *controller; struct sc_controller *controller;
@ -23,9 +23,9 @@ struct sc_keyboard_inject {
}; };
void void
sc_keyboard_inject_init(struct sc_keyboard_inject *ki, sc_keyboard_sdk_init(struct sc_keyboard_sdk *kb,
struct sc_controller *controller, struct sc_controller *controller,
enum sc_key_inject_mode key_inject_mode, enum sc_key_inject_mode key_inject_mode,
bool forward_key_repeat); bool forward_key_repeat);
#endif #endif

View File

@ -1,4 +1,4 @@
#include "mouse_inject.h" #include "mouse_sdk.h"
#include <assert.h> #include <assert.h>
@ -9,8 +9,8 @@
#include "util/intmap.h" #include "util/intmap.h"
#include "util/log.h" #include "util/log.h"
/** Downcast mouse processor to sc_mouse_inject */ /** Downcast mouse processor to sc_mouse_sdk */
#define DOWNCAST(MP) container_of(MP, struct sc_mouse_inject, mouse_processor) #define DOWNCAST(MP) container_of(MP, struct sc_mouse_sdk, mouse_processor)
static enum android_motionevent_buttons static enum android_motionevent_buttons
convert_mouse_buttons(uint32_t state) { convert_mouse_buttons(uint32_t state) {
@ -63,7 +63,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
return; return;
} }
struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_mouse_sdk *m = DOWNCAST(mp);
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@ -76,7 +76,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
}, },
}; };
if (!sc_controller_push_msg(mi->controller, &msg)) { if (!sc_controller_push_msg(m->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'"); LOGW("Could not request 'inject mouse motion event'");
} }
} }
@ -84,7 +84,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
static void static void
sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp, sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
const struct sc_mouse_click_event *event) { const struct sc_mouse_click_event *event) {
struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_mouse_sdk *m = DOWNCAST(mp);
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@ -98,7 +98,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
}, },
}; };
if (!sc_controller_push_msg(mi->controller, &msg)) { if (!sc_controller_push_msg(m->controller, &msg)) {
LOGW("Could not request 'inject mouse click event'"); LOGW("Could not request 'inject mouse click event'");
} }
} }
@ -106,7 +106,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
static void static void
sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
const struct sc_mouse_scroll_event *event) { const struct sc_mouse_scroll_event *event) {
struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_mouse_sdk *m = DOWNCAST(mp);
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
@ -118,7 +118,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
}, },
}; };
if (!sc_controller_push_msg(mi->controller, &msg)) { if (!sc_controller_push_msg(m->controller, &msg)) {
LOGW("Could not request 'inject mouse scroll event'"); LOGW("Could not request 'inject mouse scroll event'");
} }
} }
@ -126,7 +126,7 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
static void static void
sc_mouse_processor_process_touch(struct sc_mouse_processor *mp, sc_mouse_processor_process_touch(struct sc_mouse_processor *mp,
const struct sc_touch_event *event) { const struct sc_touch_event *event) {
struct sc_mouse_inject *mi = DOWNCAST(mp); struct sc_mouse_sdk *m = DOWNCAST(mp);
struct sc_control_msg msg = { struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, .type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@ -139,15 +139,14 @@ sc_mouse_processor_process_touch(struct sc_mouse_processor *mp,
}, },
}; };
if (!sc_controller_push_msg(mi->controller, &msg)) { if (!sc_controller_push_msg(m->controller, &msg)) {
LOGW("Could not request 'inject touch event'"); LOGW("Could not request 'inject touch event'");
} }
} }
void void
sc_mouse_inject_init(struct sc_mouse_inject *mi, sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller) {
struct sc_controller *controller) { m->controller = controller;
mi->controller = controller;
static const struct sc_mouse_processor_ops ops = { static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion, .process_mouse_motion = sc_mouse_processor_process_mouse_motion,
@ -156,7 +155,7 @@ sc_mouse_inject_init(struct sc_mouse_inject *mi,
.process_touch = sc_mouse_processor_process_touch, .process_touch = sc_mouse_processor_process_touch,
}; };
mi->mouse_processor.ops = &ops; m->mouse_processor.ops = &ops;
mi->mouse_processor.relative_mode = false; m->mouse_processor.relative_mode = false;
} }

View File

@ -1,5 +1,5 @@
#ifndef SC_MOUSE_INJECT_H #ifndef SC_MOUSE_SDK_H
#define SC_MOUSE_INJECT_H #define SC_MOUSE_SDK_H
#include "common.h" #include "common.h"
@ -9,14 +9,13 @@
#include "screen.h" #include "screen.h"
#include "trait/mouse_processor.h" #include "trait/mouse_processor.h"
struct sc_mouse_inject { struct sc_mouse_sdk {
struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_controller *controller; struct sc_controller *controller;
}; };
void void
sc_mouse_inject_init(struct sc_mouse_inject *mi, sc_mouse_sdk_init(struct sc_mouse_sdk *m, struct sc_controller *controller);
struct sc_controller *controller);
#endif #endif

View File

@ -21,8 +21,8 @@ const struct scrcpy_options scrcpy_options_default = {
.video_source = SC_VIDEO_SOURCE_DISPLAY, .video_source = SC_VIDEO_SOURCE_DISPLAY,
.audio_source = SC_AUDIO_SOURCE_AUTO, .audio_source = SC_AUDIO_SOURCE_AUTO,
.record_format = SC_RECORD_FORMAT_AUTO, .record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO,
.camera_facing = SC_CAMERA_FACING_ANY, .camera_facing = SC_CAMERA_FACING_ANY,
.port_range = { .port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
@ -89,6 +89,7 @@ const struct scrcpy_options scrcpy_options_default = {
.kill_adb_on_close = false, .kill_adb_on_close = false,
.camera_high_speed = false, .camera_high_speed = false,
.list = 0, .list = 0,
.window = true,
}; };
enum sc_orientation enum sc_orientation

View File

@ -140,13 +140,19 @@ enum sc_lock_video_orientation {
}; };
enum sc_keyboard_input_mode { enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_INJECT, SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_HID, SC_KEYBOARD_INPUT_MODE_DISABLED,
SC_KEYBOARD_INPUT_MODE_SDK,
SC_KEYBOARD_INPUT_MODE_UHID,
SC_KEYBOARD_INPUT_MODE_AOA,
}; };
enum sc_mouse_input_mode { enum sc_mouse_input_mode {
SC_MOUSE_INPUT_MODE_INJECT, SC_MOUSE_INPUT_MODE_AUTO,
SC_MOUSE_INPUT_MODE_HID, SC_MOUSE_INPUT_MODE_DISABLED,
SC_MOUSE_INPUT_MODE_SDK,
SC_MOUSE_INPUT_MODE_UHID,
SC_MOUSE_INPUT_MODE_AOA,
}; };
enum sc_key_inject_mode { enum sc_key_inject_mode {
@ -273,6 +279,7 @@ struct scrcpy_options {
#define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8 #define SC_OPTION_LIST_CAMERA_SIZES 0x8
uint8_t list; uint8_t list;
bool window;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

View File

@ -1,21 +1,24 @@
#include "receiver.h" #include "receiver.h"
#include <assert.h> #include <assert.h>
#include <inttypes.h>
#include <stdint.h>
#include <SDL2/SDL_clipboard.h> #include <SDL2/SDL_clipboard.h>
#include "device_msg.h" #include "device_msg.h"
#include "util/log.h" #include "util/log.h"
#include "util/str.h"
bool bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket) {
struct sc_acksync *acksync) {
bool ok = sc_mutex_init(&receiver->mutex); bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) { if (!ok) {
return false; return false;
} }
receiver->control_socket = control_socket; receiver->control_socket = control_socket;
receiver->acksync = acksync; receiver->acksync = NULL;
receiver->uhid_devices = NULL;
return true; return true;
} }
@ -26,7 +29,7 @@ sc_receiver_destroy(struct sc_receiver *receiver) {
} }
static void static void
process_msg(struct sc_receiver *receiver, struct device_msg *msg) { process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
switch (msg->type) { switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: { case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText(); char *current = SDL_GetClipboardText();
@ -42,20 +45,65 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
break; break;
} }
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: case DEVICE_MSG_TYPE_ACK_CLIPBOARD:
assert(receiver->acksync);
LOGD("Ack device clipboard sequence=%" PRIu64_, LOGD("Ack device clipboard sequence=%" PRIu64_,
msg->ack_clipboard.sequence); msg->ack_clipboard.sequence);
// This is a programming error to receive this message if there is
// no ACK synchronization mechanism
assert(receiver->acksync);
// Also check at runtime (do not trust the server)
if (!receiver->acksync) {
LOGE("Received unexpected ack");
return;
}
sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence); sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence);
break; break;
case DEVICE_MSG_TYPE_UHID_OUTPUT:
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
char *hex = sc_str_to_hex_string(msg->uhid_output.data,
msg->uhid_output.size);
if (hex) {
LOGV("UHID output [%" PRIu16 "] %s",
msg->uhid_output.id, hex);
free(hex);
} else {
LOGV("UHID output [%" PRIu16 "] size=%" PRIu16,
msg->uhid_output.id, msg->uhid_output.size);
}
}
// This is a programming error to receive this message if there is
// no uhid_devices instance
assert(receiver->uhid_devices);
// Also check at runtime (do not trust the server)
if (!receiver->uhid_devices) {
LOGE("Received unexpected HID output message");
return;
}
struct sc_uhid_receiver *uhid_receiver =
sc_uhid_devices_get_receiver(receiver->uhid_devices,
msg->uhid_output.id);
if (uhid_receiver) {
uhid_receiver->ops->process_output(uhid_receiver,
msg->uhid_output.data,
msg->uhid_output.size);
} else {
LOGW("No UHID receiver for id %" PRIu16, msg->uhid_output.id);
}
break;
} }
} }
static ssize_t static ssize_t
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) { process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) {
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(&buf[head], len - head, &msg); ssize_t r = sc_device_msg_deserialize(&buf[head], len - head, &msg);
if (r == -1) { if (r == -1) {
return -1; return -1;
} }
@ -64,7 +112,7 @@ process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len)
} }
process_msg(receiver, &msg); process_msg(receiver, &msg);
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
head += r; head += r;
assert(head <= len); assert(head <= len);
@ -78,7 +126,7 @@ static int
run_receiver(void *data) { run_receiver(void *data) {
struct sc_receiver *receiver = data; struct sc_receiver *receiver = data;
static unsigned char buf[DEVICE_MSG_MAX_SIZE]; static uint8_t buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0; size_t head = 0;
for (;;) { for (;;) {

View File

@ -5,6 +5,7 @@
#include <stdbool.h> #include <stdbool.h>
#include "uhid/uhid_output.h"
#include "util/acksync.h" #include "util/acksync.h"
#include "util/net.h" #include "util/net.h"
#include "util/thread.h" #include "util/thread.h"
@ -17,11 +18,11 @@ struct sc_receiver {
sc_mutex mutex; sc_mutex mutex;
struct sc_acksync *acksync; struct sc_acksync *acksync;
struct sc_uhid_devices *uhid_devices;
}; };
bool bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket, sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket);
struct sc_acksync *acksync);
void void
sc_receiver_destroy(struct sc_receiver *receiver); sc_receiver_destroy(struct sc_receiver *receiver);

View File

@ -20,15 +20,17 @@
#include "demuxer.h" #include "demuxer.h"
#include "events.h" #include "events.h"
#include "file_pusher.h" #include "file_pusher.h"
#include "keyboard_inject.h" #include "keyboard_sdk.h"
#include "mouse_inject.h" #include "mouse_sdk.h"
#include "recorder.h" #include "recorder.h"
#include "screen.h" #include "screen.h"
#include "server.h" #include "server.h"
#include "uhid/keyboard_uhid.h"
#include "uhid/mouse_uhid.h"
#ifdef HAVE_USB #ifdef HAVE_USB
# include "usb/aoa_hid.h" # include "usb/aoa_hid.h"
# include "usb/hid_keyboard.h" # include "usb/keyboard_aoa.h"
# include "usb/hid_mouse.h" # include "usb/mouse_aoa.h"
# include "usb/usb.h" # include "usb/usb.h"
#endif #endif
#include "util/acksync.h" #include "util/acksync.h"
@ -61,17 +63,20 @@ struct scrcpy {
struct sc_aoa aoa; struct sc_aoa aoa;
// sequence/ack helper to synchronize clipboard and Ctrl+v via HID // sequence/ack helper to synchronize clipboard and Ctrl+v via HID
struct sc_acksync acksync; struct sc_acksync acksync;
struct sc_uhid_devices uhid_devices;
#endif #endif
union { union {
struct sc_keyboard_inject keyboard_inject; struct sc_keyboard_sdk keyboard_sdk;
struct sc_keyboard_uhid keyboard_uhid;
#ifdef HAVE_USB #ifdef HAVE_USB
struct sc_hid_keyboard keyboard_hid; struct sc_keyboard_aoa keyboard_aoa;
#endif #endif
}; };
union { union {
struct sc_mouse_inject mouse_inject; struct sc_mouse_sdk mouse_sdk;
struct sc_mouse_uhid mouse_uhid;
#ifdef HAVE_USB #ifdef HAVE_USB
struct sc_hid_mouse mouse_hid; struct sc_mouse_aoa mouse_aoa;
#endif #endif
}; };
struct sc_timeout timeout; struct sc_timeout timeout;
@ -101,7 +106,6 @@ static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
static void static void
sdl_set_hints(const char *render_driver) { sdl_set_hints(const char *render_driver) {
if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) { if (render_driver && !SDL_SetHint(SDL_HINT_RENDER_DRIVER, render_driver)) {
LOGW("Could not set render driver"); LOGW("Could not set render driver");
} }
@ -307,6 +311,10 @@ scrcpy_generate_scid(void) {
enum scrcpy_exit_code enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) { scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy; static struct scrcpy scrcpy;
#ifndef NDEBUG
// Detect missing initializations
memset(&scrcpy, 42, sizeof(scrcpy));
#endif
struct scrcpy *s = &scrcpy; struct scrcpy *s = &scrcpy;
// Minimal SDL initialization // Minimal SDL initialization
@ -330,8 +338,8 @@ scrcpy(struct scrcpy_options *options) {
bool audio_demuxer_started = false; bool audio_demuxer_started = false;
#ifdef HAVE_USB #ifdef HAVE_USB
bool aoa_hid_initialized = false; bool aoa_hid_initialized = false;
bool hid_keyboard_initialized = false; bool keyboard_aoa_initialized = false;
bool hid_mouse_initialized = false; bool mouse_aoa_initialized = false;
#endif #endif
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
@ -340,6 +348,7 @@ scrcpy(struct scrcpy_options *options) {
bool timeout_started = false; bool timeout_started = false;
struct sc_acksync *acksync = NULL; struct sc_acksync *acksync = NULL;
struct sc_uhid_devices *uhid_devices = NULL;
uint32_t scid = scrcpy_generate_scid(); uint32_t scid = scrcpy_generate_scid();
@ -399,6 +408,12 @@ scrcpy(struct scrcpy_options *options) {
return SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
} }
if (options->video_playback) {
// Set hints before starting the server thread to avoid race conditions
// in SDL
sdl_set_hints(options->render_driver);
}
if (!sc_server_start(&s->server)) { if (!sc_server_start(&s->server)) {
goto end; goto end;
} }
@ -415,11 +430,7 @@ scrcpy(struct scrcpy_options *options) {
assert(!options->video_playback || options->video); assert(!options->video_playback || options->video);
assert(!options->audio_playback || options->audio); assert(!options->audio_playback || options->audio);
if (options->video_playback) { if (options->window ||
sdl_set_hints(options->render_driver);
}
if (options->video_playback ||
(options->control && options->clipboard_autosync)) { (options->control && options->clipboard_autosync)) {
// Initialize the video subsystem even if --no-video or // Initialize the video subsystem even if --no-video or
// --no-video-playback is passed so that clipboard synchronization // --no-video-playback is passed so that clipboard synchronization
@ -542,12 +553,19 @@ scrcpy(struct scrcpy_options *options) {
struct sc_mouse_processor *mp = NULL; struct sc_mouse_processor *mp = NULL;
if (options->control) { if (options->control) {
if (!sc_controller_init(&s->controller, s->server.control_socket)) {
goto end;
}
controller_initialized = true;
controller = &s->controller;
#ifdef HAVE_USB #ifdef HAVE_USB
bool use_hid_keyboard = bool use_keyboard_aoa =
options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA;
bool use_hid_mouse = bool use_mouse_aoa =
options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA;
if (use_hid_keyboard || use_hid_mouse) { if (use_keyboard_aoa || use_mouse_aoa) {
bool ok = sc_acksync_init(&s->acksync); bool ok = sc_acksync_init(&s->acksync);
if (!ok) { if (!ok) {
goto end; goto end;
@ -557,7 +575,7 @@ scrcpy(struct scrcpy_options *options) {
if (!ok) { if (!ok) {
LOGE("Failed to initialize USB"); LOGE("Failed to initialize USB");
sc_acksync_destroy(&s->acksync); sc_acksync_destroy(&s->acksync);
goto aoa_hid_end; goto end;
} }
assert(serial); assert(serial);
@ -565,7 +583,7 @@ scrcpy(struct scrcpy_options *options) {
ok = sc_usb_select_device(&s->usb, serial, &usb_device); ok = sc_usb_select_device(&s->usb, serial, &usb_device);
if (!ok) { if (!ok) {
sc_usb_destroy(&s->usb); sc_usb_destroy(&s->usb);
goto aoa_hid_end; goto end;
} }
LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s", LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s",
@ -578,7 +596,7 @@ scrcpy(struct scrcpy_options *options) {
LOGE("Failed to connect to USB device %s", serial); LOGE("Failed to connect to USB device %s", serial);
sc_usb_destroy(&s->usb); sc_usb_destroy(&s->usb);
sc_acksync_destroy(&s->acksync); sc_acksync_destroy(&s->acksync);
goto aoa_hid_end; goto end;
} }
ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync); ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync);
@ -587,105 +605,91 @@ scrcpy(struct scrcpy_options *options) {
sc_usb_disconnect(&s->usb); sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb); sc_usb_destroy(&s->usb);
sc_acksync_destroy(&s->acksync); sc_acksync_destroy(&s->acksync);
goto aoa_hid_end; goto end;
} }
if (use_hid_keyboard) { if (use_keyboard_aoa) {
if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) { if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) {
hid_keyboard_initialized = true; keyboard_aoa_initialized = true;
kp = &s->keyboard_hid.key_processor; kp = &s->keyboard_aoa.key_processor;
} else { } else {
LOGE("Could not initialize HID keyboard"); LOGE("Could not initialize HID keyboard");
} }
} }
if (use_hid_mouse) { if (use_mouse_aoa) {
if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) { if (sc_mouse_aoa_init(&s->mouse_aoa, &s->aoa)) {
hid_mouse_initialized = true; mouse_aoa_initialized = true;
mp = &s->mouse_hid.mouse_processor; mp = &s->mouse_aoa.mouse_processor;
} else { } else {
LOGE("Could not initialized HID mouse"); LOGE("Could not initialized HID mouse");
} }
} }
bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized; bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized;
if (!need_aoa || !sc_aoa_start(&s->aoa)) { if (!need_aoa || !sc_aoa_start(&s->aoa)) {
sc_acksync_destroy(&s->acksync); sc_acksync_destroy(&s->acksync);
sc_usb_disconnect(&s->usb); sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb); sc_usb_destroy(&s->usb);
sc_aoa_destroy(&s->aoa); sc_aoa_destroy(&s->aoa);
goto aoa_hid_end; goto end;
} }
acksync = &s->acksync; acksync = &s->acksync;
aoa_hid_initialized = true; aoa_hid_initialized = true;
aoa_hid_end:
if (!aoa_hid_initialized) {
if (hid_keyboard_initialized) {
sc_hid_keyboard_destroy(&s->keyboard_hid);
hid_keyboard_initialized = false;
}
if (hid_mouse_initialized) {
sc_hid_mouse_destroy(&s->mouse_hid);
hid_mouse_initialized = false;
}
}
if (use_hid_keyboard && !hid_keyboard_initialized) {
LOGE("Fallback to default keyboard injection method "
"(-K/--hid-keyboard ignored)");
options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT;
}
if (use_hid_mouse && !hid_mouse_initialized) {
LOGE("Fallback to default mouse injection method "
"(-M/--hid-mouse ignored)");
options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT;
}
} }
#else #else
assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID); assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_AOA);
assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID); assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA);
#endif #endif
// keyboard_input_mode may have been reset if HID mode failed if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) {
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) { sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller,
sc_keyboard_inject_init(&s->keyboard_inject, &s->controller, options->key_inject_mode,
options->key_inject_mode, options->forward_key_repeat);
options->forward_key_repeat); kp = &s->keyboard_sdk.key_processor;
kp = &s->keyboard_inject.key_processor; } else if (options->keyboard_input_mode
== SC_KEYBOARD_INPUT_MODE_UHID) {
sc_uhid_devices_init(&s->uhid_devices);
bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller,
&s->uhid_devices);
if (!ok) {
goto end;
}
uhid_devices = &s->uhid_devices;
kp = &s->keyboard_uhid.key_processor;
} }
// mouse_input_mode may have been reset if HID mode failed if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) { sc_mouse_sdk_init(&s->mouse_sdk, &s->controller);
sc_mouse_inject_init(&s->mouse_inject, &s->controller); mp = &s->mouse_sdk.mouse_processor;
mp = &s->mouse_inject.mouse_processor; } else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) {
bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller);
if (!ok) {
goto end;
}
mp = &s->mouse_uhid.mouse_processor;
} }
if (!sc_controller_init(&s->controller, s->server.control_socket, sc_controller_configure(&s->controller, acksync, uhid_devices);
acksync)) {
goto end;
}
controller_initialized = true;
if (!sc_controller_start(&s->controller)) { if (!sc_controller_start(&s->controller)) {
goto end; goto end;
} }
controller_started = true; controller_started = true;
controller = &s->controller;
} }
// There is a controller if and only if control is enabled // There is a controller if and only if control is enabled
assert(options->control == !!controller); assert(options->control == !!controller);
if (options->video_playback) { if (options->window) {
const char *window_title = const char *window_title =
options->window_title ? options->window_title : info->device_name; options->window_title ? options->window_title : info->device_name;
struct sc_screen_params screen_params = { struct sc_screen_params screen_params = {
.video = options->video_playback,
.controller = controller, .controller = controller,
.fp = fp, .fp = fp,
.kp = kp, .kp = kp,
@ -707,12 +711,15 @@ aoa_hid_end:
.start_fps_counter = options->start_fps_counter, .start_fps_counter = options->start_fps_counter,
}; };
struct sc_frame_source *src = &s->video_decoder.frame_source; struct sc_frame_source *src;
if (options->display_buffer) { if (options->video_playback) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer, src = &s->video_decoder.frame_source;
true); if (options->display_buffer) {
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); sc_delay_buffer_init(&s->display_buffer,
src = &s->display_buffer.frame_source; 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)) { if (!sc_screen_init(&s->screen, &screen_params)) {
@ -720,7 +727,9 @@ aoa_hid_end:
} }
screen_initialized = true; screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink); if (options->video_playback) {
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
} }
if (options->audio_playback) { if (options->audio_playback) {
@ -802,9 +811,12 @@ aoa_hid_end:
ret = event_loop(s); ret = event_loop(s);
LOGD("quit..."); LOGD("quit...");
// Close the window immediately on closing, because screen_destroy() may if (options->video_playback) {
// only be called once the video demuxer thread is joined (it may take time) // Close the window immediately on closing, because screen_destroy()
sc_screen_hide_window(&s->screen); // may only be called once the video demuxer thread is joined (it may
// take time)
sc_screen_hide_window(&s->screen);
}
end: end:
if (timeout_started) { if (timeout_started) {
@ -815,11 +827,11 @@ end:
// end-of-stream // end-of-stream
#ifdef HAVE_USB #ifdef HAVE_USB
if (aoa_hid_initialized) { if (aoa_hid_initialized) {
if (hid_keyboard_initialized) { if (keyboard_aoa_initialized) {
sc_hid_keyboard_destroy(&s->keyboard_hid); sc_keyboard_aoa_destroy(&s->keyboard_aoa);
} }
if (hid_mouse_initialized) { if (mouse_aoa_initialized) {
sc_hid_mouse_destroy(&s->mouse_hid); sc_mouse_aoa_destroy(&s->mouse_aoa);
} }
sc_aoa_stop(&s->aoa); sc_aoa_stop(&s->aoa);
sc_usb_stop(&s->usb); sc_usb_stop(&s->usb);

View File

@ -205,6 +205,8 @@ sc_screen_toggle_mouse_capture(struct sc_screen *screen) {
static void static void
sc_screen_update_content_rect(struct sc_screen *screen) { sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
int dw; int dw;
int dh; int dh;
SDL_GL_GetDrawableSize(screen->window, &dw, &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 // changed, so that the content rectangle is recomputed
static void static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) { sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->video);
if (update_content_rect) { if (update_content_rect) {
sc_screen_update_content_rect(screen); sc_screen_update_content_rect(screen);
} }
@ -268,6 +272,8 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
static int static int
event_watcher(void *data, SDL_Event *event) { event_watcher(void *data, SDL_Event *event) {
struct sc_screen *screen = data; struct sc_screen *screen = data;
assert(screen->video);
if (event->type == SDL_WINDOWEVENT if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) { && event->window.event == SDL_WINDOWEVENT_RESIZED) {
// In practice, it seems to always be called from the same thread in // In practice, it seems to always be called from the same thread in
@ -326,6 +332,7 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
static bool static bool
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_screen *screen = DOWNCAST(sink); struct sc_screen *screen = DOWNCAST(sink);
assert(screen->video);
bool previous_skipped; bool previous_skipped;
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped); bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
@ -362,6 +369,10 @@ sc_screen_init(struct sc_screen *screen,
screen->maximized = false; screen->maximized = false;
screen->minimized = false; screen->minimized = false;
screen->mouse_capture_key_pressed = 0; screen->mouse_capture_key_pressed = 0;
screen->paused = false;
screen->resume_frame = NULL;
screen->video = params->video;
screen->req.x = params->window_x; screen->req.x = params->window_x;
screen->req.y = params->window_y; screen->req.y = params->window_y;
@ -385,9 +396,7 @@ sc_screen_init(struct sc_screen *screen,
sc_orientation_get_name(screen->orientation)); sc_orientation_get_name(screen->orientation));
} }
uint32_t window_flags = SDL_WINDOW_HIDDEN uint32_t window_flags = SDL_WINDOW_ALLOW_HIGHDPI;
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_ALLOW_HIGHDPI;
if (params->always_on_top) { if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
} }
@ -395,25 +404,58 @@ sc_screen_init(struct sc_screen *screen,
window_flags |= SDL_WINDOW_BORDERLESS; window_flags |= SDL_WINDOW_BORDERLESS;
} }
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->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE;
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 // The window will be positioned and sized on first video frame
screen->window = screen->window = SDL_CreateWindow(title, x, y, width, height, window_flags);
SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags);
if (!screen->window) { if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError()); LOGE("Could not create window: %s", SDL_GetError());
goto error_destroy_fps_counter; 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(); SDL_Surface *icon = scrcpy_icon_load();
if (icon) { if (icon) {
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
scrcpy_icon_destroy(icon); } else if (params->video) {
} else { // just a warning
LOGW("Could not load icon"); 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;
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
params->mipmaps);
if (icon) {
scrcpy_icon_destroy(icon);
}
if (!ok) {
goto error_destroy_window;
} }
screen->frame = av_frame_alloc(); screen->frame = av_frame_alloc();
@ -437,7 +479,9 @@ sc_screen_init(struct sc_screen *screen,
sc_input_manager_init(&screen->im, &im_params); sc_input_manager_init(&screen->im, &im_params);
#ifdef CONTINUOUS_RESIZING_WORKAROUND #ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, screen); if (screen->video) {
SDL_AddEventWatch(event_watcher, screen);
}
#endif #endif
static const struct sc_frame_sink_ops ops = { static const struct sc_frame_sink_ops ops = {
@ -452,6 +496,11 @@ sc_screen_init(struct sc_screen *screen,
screen->open = false; screen->open = false;
#endif #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; return true;
error_destroy_display: error_destroy_display:
@ -522,6 +571,8 @@ sc_screen_destroy(struct sc_screen *screen) {
static void static void
resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
struct sc_size new_content_size) { struct sc_size new_content_size) {
assert(screen->video);
struct sc_size window_size = get_window_size(screen); struct sc_size window_size = get_window_size(screen);
struct sc_size target_size = { struct sc_size target_size = {
.width = (uint32_t) window_size.width * new_content_size.width .width = (uint32_t) window_size.width * new_content_size.width
@ -535,6 +586,8 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
static void static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
assert(screen->video);
if (!screen->fullscreen && !screen->maximized && !screen->minimized) { if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size); resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) { } else if (!screen->resize_pending) {
@ -549,6 +602,8 @@ set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
static void static void
apply_pending_resize(struct sc_screen *screen) { apply_pending_resize(struct sc_screen *screen) {
assert(screen->video);
assert(!screen->fullscreen); assert(!screen->fullscreen);
assert(!screen->maximized); assert(!screen->maximized);
assert(!screen->minimized); assert(!screen->minimized);
@ -562,6 +617,8 @@ apply_pending_resize(struct sc_screen *screen) {
void void
sc_screen_set_orientation(struct sc_screen *screen, sc_screen_set_orientation(struct sc_screen *screen,
enum sc_orientation orientation) { enum sc_orientation orientation) {
assert(screen->video);
if (orientation == screen->orientation) { if (orientation == screen->orientation) {
return; return;
} }
@ -596,6 +653,8 @@ sc_screen_init_size(struct sc_screen *screen) {
// recreate the texture and resize the window if the frame size has changed // recreate the texture and resize the window if the frame size has changed
static enum sc_display_result static enum sc_display_result
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { 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 if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) { && screen->frame_size.height == new_frame_size.height) {
return SC_DISPLAY_RESULT_OK; return SC_DISPLAY_RESULT_OK;
@ -614,13 +673,12 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
} }
static bool static bool
sc_screen_update_frame(struct sc_screen *screen) { sc_screen_apply_frame(struct sc_screen *screen) {
av_frame_unref(screen->frame); assert(screen->video);
sc_frame_buffer_consume(&screen->fb, screen->frame);
AVFrame *frame = screen->frame;
sc_fps_counter_add_rendered_frame(&screen->fps_counter); sc_fps_counter_add_rendered_frame(&screen->fps_counter);
AVFrame *frame = screen->frame;
struct sc_size new_frame_size = {frame->width, frame->height}; struct sc_size new_frame_size = {frame->width, frame->height};
enum sc_display_result res = prepare_for_frame(screen, new_frame_size); enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
if (res == SC_DISPLAY_RESULT_ERROR) { if (res == SC_DISPLAY_RESULT_ERROR) {
@ -655,8 +713,62 @@ sc_screen_update_frame(struct sc_screen *screen) {
return true; return true;
} }
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();
if (!screen->resume_frame) {
LOG_OOM();
return false;
}
} else {
av_frame_unref(screen->resume_frame);
}
sc_frame_buffer_consume(&screen->fb, screen->resume_frame);
return true;
}
av_frame_unref(screen->frame);
sc_frame_buffer_consume(&screen->fb, screen->frame);
return sc_screen_apply_frame(screen);
}
void
sc_screen_set_paused(struct sc_screen *screen, bool paused) {
assert(screen->video);
if (!paused && !screen->paused) {
// nothing to do
return;
}
if (screen->paused && screen->resume_frame) {
// If display screen was paused, refresh the frame immediately, even if
// the new state is also paused.
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
sc_screen_apply_frame(screen);
}
if (!paused) {
LOGI("Display screen unpaused");
} else if (!screen->paused) {
LOGI("Display screen paused");
} else {
LOGI("Display screen re-paused");
}
screen->paused = paused;
}
void void
sc_screen_switch_fullscreen(struct sc_screen *screen) { sc_screen_switch_fullscreen(struct sc_screen *screen) {
assert(screen->video);
uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP;
if (SDL_SetWindowFullscreen(screen->window, new_mode)) { if (SDL_SetWindowFullscreen(screen->window, new_mode)) {
LOGW("Could not switch fullscreen mode: %s", SDL_GetError()); LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
@ -674,6 +786,8 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
void void
sc_screen_resize_to_fit(struct sc_screen *screen) { sc_screen_resize_to_fit(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->maximized || screen->minimized) { if (screen->fullscreen || screen->maximized || screen->minimized) {
return; return;
} }
@ -698,6 +812,8 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
void void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
assert(screen->video);
if (screen->fullscreen || screen->minimized) { if (screen->fullscreen || screen->minimized) {
return; return;
} }
@ -741,6 +857,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
return true; return true;
} }
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
// !video implies !has_frame
assert(screen->video || !screen->has_frame);
if (!screen->has_frame) { if (!screen->has_frame) {
// Do nothing // Do nothing
return true; return true;
@ -844,6 +962,8 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
struct sc_point struct sc_point
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) { int32_t x, int32_t y) {
assert(screen->video);
enum sc_orientation orientation = screen->orientation; enum sc_orientation orientation = screen->orientation;
int32_t w = screen->content_size.width; 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 bool open; // track the open/close state to assert correct behavior
#endif #endif
bool video;
struct sc_display display; struct sc_display display;
struct sc_input_manager im; struct sc_input_manager im;
struct sc_frame_buffer fb; struct sc_frame_buffer fb;
@ -64,9 +66,14 @@ struct sc_screen {
SDL_Keycode mouse_capture_key_pressed; SDL_Keycode mouse_capture_key_pressed;
AVFrame *frame; AVFrame *frame;
bool paused;
AVFrame *resume_frame;
}; };
struct sc_screen_params { struct sc_screen_params {
bool video;
struct sc_controller *controller; struct sc_controller *controller;
struct sc_file_pusher *fp; struct sc_file_pusher *fp;
struct sc_key_processor *kp; struct sc_key_processor *kp;
@ -135,6 +142,10 @@ void
sc_screen_set_orientation(struct sc_screen *screen, sc_screen_set_orientation(struct sc_screen *screen,
enum sc_orientation orientation); enum sc_orientation orientation);
// set the display pause state
void
sc_screen_set_paused(struct sc_screen *screen, bool paused);
// react to SDL events // react to SDL events
// If this function returns false, scrcpy must exit with an error. // If this function returns false, scrcpy must exit with an error.
bool bool

View File

@ -498,7 +498,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
static bool static bool
device_read_info(struct sc_intr *intr, sc_socket device_socket, device_read_info(struct sc_intr *intr, sc_socket device_socket,
struct sc_server_info *info) { struct sc_server_info *info) {
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH]; uint8_t buf[SC_DEVICE_NAME_FIELD_LENGTH];
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf)); ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
if (r < SC_DEVICE_NAME_FIELD_LENGTH) { if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
LOGE("Could not retrieve device information"); LOGE("Could not retrieve device information");

View File

@ -23,6 +23,13 @@ struct sc_key_processor {
*/ */
bool async_paste; bool async_paste;
/**
* Set by the implementation to indicate that the keyboard is HID. In
* practice, it is used to react on a shortcut to open the hard keyboard
* settings only if the keyboard is HID.
*/
bool hid;
const struct sc_key_processor_ops *ops; const struct sc_key_processor_ops *ops;
}; };

View File

@ -0,0 +1,162 @@
#include "keyboard_uhid.h"
#include "util/log.h"
/** Downcast key processor to keyboard_uhid */
#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor)
/** Downcast uhid_receiver to keyboard_uhid */
#define DOWNCAST_RECEIVER(UR) \
container_of(UR, struct sc_keyboard_uhid, uhid_receiver)
#define UHID_KEYBOARD_ID 1
static void
sc_keyboard_uhid_send_input(struct sc_keyboard_uhid *kb,
const struct sc_hid_event *event) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = UHID_KEYBOARD_ID;
assert(event->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, event->data, event->size);
msg.uhid_input.size = event->size;
if (!sc_controller_push_msg(kb->controller, &msg)) {
LOGE("Could not send UHID_INPUT message (key)");
}
}
static void
sc_keyboard_uhid_synchronize_mod(struct sc_keyboard_uhid *kb) {
SDL_Keymod sdl_mod = SDL_GetModState();
uint16_t mod = sc_mods_state_from_sdl(sdl_mod) & (SC_MOD_CAPS | SC_MOD_NUM);
uint16_t device_mod =
atomic_load_explicit(&kb->device_mod, memory_order_relaxed);
uint16_t diff = mod ^ device_mod;
if (diff) {
// Inherently racy (the HID output reports arrive asynchronously in
// response to key presses), but will re-synchronize on next key press
// or HID output anyway
atomic_store_explicit(&kb->device_mod, mod, memory_order_relaxed);
struct sc_hid_event hid_event;
sc_hid_keyboard_event_from_mods(&hid_event, diff);
LOGV("HID keyboard state synchronized");
sc_keyboard_uhid_send_input(kb, &hid_event);
}
}
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const struct sc_key_event *event,
uint64_t ack_to_wait) {
(void) ack_to_wait;
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
return;
}
struct sc_keyboard_uhid *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
// Not all keys are supported, just ignore unsupported keys
if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) {
if (event->scancode == SC_SCANCODE_CAPSLOCK) {
atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS,
memory_order_relaxed);
} else if (event->scancode == SC_SCANCODE_NUMLOCK) {
atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM,
memory_order_relaxed);
} else {
// Synchronize modifiers (only if the scancode itself does not
// change the modifiers)
sc_keyboard_uhid_synchronize_mod(kb);
}
sc_keyboard_uhid_send_input(kb, &hid_event);
}
}
static unsigned
sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) {
// <https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf>
// (chapter 11: LED page)
unsigned mod = 0;
if (hid_led & 0x01) {
mod |= SC_MOD_NUM;
}
if (hid_led & 0x02) {
mod |= SC_MOD_CAPS;
}
return mod;
}
static void
sc_uhid_receiver_process_output(struct sc_uhid_receiver *receiver,
const uint8_t *data, size_t len) {
// Called from the thread receiving device messages
assert(len);
// Also check at runtime (do not trust the server)
if (!len) {
LOGE("Unexpected empty HID output message");
return;
}
struct sc_keyboard_uhid *kb = DOWNCAST_RECEIVER(receiver);
uint8_t hid_led = data[0];
uint16_t device_mod = sc_keyboard_uhid_to_sc_mod(hid_led);
atomic_store_explicit(&kb->device_mod, device_mod, memory_order_relaxed);
}
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller,
struct sc_uhid_devices *uhid_devices) {
sc_hid_keyboard_init(&kb->hid);
kb->controller = controller;
atomic_init(&kb->device_mod, 0);
static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key,
// Never forward text input via HID (all the keys are injected
// separately)
.process_text = NULL,
};
// Clipboard synchronization is requested over the same control socket, so
// there is no need for a specific synchronization mechanism
kb->key_processor.async_paste = false;
kb->key_processor.hid = true;
kb->key_processor.ops = &ops;
static const struct sc_uhid_receiver_ops uhid_receiver_ops = {
.process_output = sc_uhid_receiver_process_output,
};
kb->uhid_receiver.id = UHID_KEYBOARD_ID;
kb->uhid_receiver.ops = &uhid_receiver_ops;
sc_uhid_devices_add_receiver(uhid_devices, &kb->uhid_receiver);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = UHID_KEYBOARD_ID;
msg.uhid_create.report_desc = SC_HID_KEYBOARD_REPORT_DESC;
msg.uhid_create.report_desc_size = SC_HID_KEYBOARD_REPORT_DESC_LEN;
if (!sc_controller_push_msg(controller, &msg)) {
LOGE("Could not send UHID_CREATE message (keyboard)");
return false;
}
return true;
}

View File

@ -0,0 +1,27 @@
#ifndef SC_KEYBOARD_UHID_H
#define SC_KEYBOARD_UHID_H
#include "common.h"
#include <stdbool.h>
#include "controller.h"
#include "hid/hid_keyboard.h"
#include "uhid/uhid_output.h"
#include "trait/key_processor.h"
struct sc_keyboard_uhid {
struct sc_key_processor key_processor; // key processor trait
struct sc_uhid_receiver uhid_receiver;
struct sc_hid_keyboard hid;
struct sc_controller *controller;
atomic_uint_least16_t device_mod;
};
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller,
struct sc_uhid_devices *uhid_devices);
#endif

89
app/src/uhid/mouse_uhid.c Normal file
View File

@ -0,0 +1,89 @@
#include "mouse_uhid.h"
#include "hid/hid_mouse.h"
#include "input_events.h"
#include "util/log.h"
/** Downcast mouse processor to mouse_uhid */
#define DOWNCAST(MP) container_of(MP, struct sc_mouse_uhid, mouse_processor)
#define UHID_MOUSE_ID 2
static void
sc_mouse_uhid_send_input(struct sc_mouse_uhid *mouse,
const struct sc_hid_event *event, const char *name) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = UHID_MOUSE_ID;
assert(event->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, event->data, event->size);
msg.uhid_input.size = event->size;
if (!sc_controller_push_msg(mouse->controller, &msg)) {
LOGE("Could not send UHID_INPUT message (%s)", name);
}
}
static void
sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
const struct sc_mouse_motion_event *event) {
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_motion(&hid_event, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion");
}
static void
sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
const struct sc_mouse_click_event *event) {
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_click(&hid_event, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click");
}
static void
sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
const struct sc_mouse_scroll_event *event) {
struct sc_mouse_uhid *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_scroll(&hid_event, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll");
}
bool
sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
struct sc_controller *controller) {
mouse->controller = controller;
static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion,
.process_mouse_click = sc_mouse_processor_process_mouse_click,
.process_mouse_scroll = sc_mouse_processor_process_mouse_scroll,
// Touch events not supported (coordinates are not relative)
.process_touch = NULL,
};
mouse->mouse_processor.ops = &ops;
mouse->mouse_processor.relative_mode = true;
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = UHID_MOUSE_ID;
msg.uhid_create.report_desc = SC_HID_MOUSE_REPORT_DESC;
msg.uhid_create.report_desc_size = SC_HID_MOUSE_REPORT_DESC_LEN;
if (!sc_controller_push_msg(controller, &msg)) {
LOGE("Could not send UHID_CREATE message (mouse)");
return false;
}
return true;
}

19
app/src/uhid/mouse_uhid.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef SC_MOUSE_UHID_H
#define SC_MOUSE_UHID_H
#include <stdbool.h>
#include "controller.h"
#include "trait/mouse_processor.h"
struct sc_mouse_uhid {
struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_controller *controller;
};
bool
sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
struct sc_controller *controller);
#endif

View File

@ -0,0 +1,25 @@
#include "uhid_output.h"
#include <assert.h>
void
sc_uhid_devices_init(struct sc_uhid_devices *devices) {
devices->count = 0;
}
void
sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices,
struct sc_uhid_receiver *receiver) {
assert(devices->count < SC_UHID_MAX_RECEIVERS);
devices->receivers[devices->count++] = receiver;
}
struct sc_uhid_receiver *
sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id) {
for (size_t i = 0; i < devices->count; ++i) {
if (devices->receivers[i]->id == id) {
return devices->receivers[i];
}
}
return NULL;
}

View File

@ -0,0 +1,45 @@
#ifndef SC_UHID_OUTPUT_H
#define SC_UHID_OUTPUT_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
/**
* The communication with UHID devices is bidirectional.
*
* This component manages the registration of receivers to handle UHID output
* messages (sent from the device to the computer).
*/
struct sc_uhid_receiver {
uint16_t id;
const struct sc_uhid_receiver_ops *ops;
};
struct sc_uhid_receiver_ops {
void
(*process_output)(struct sc_uhid_receiver *receiver,
const uint8_t *data, size_t len);
};
#define SC_UHID_MAX_RECEIVERS 1
struct sc_uhid_devices {
struct sc_uhid_receiver *receivers[SC_UHID_MAX_RECEIVERS];
unsigned count;
};
void
sc_uhid_devices_init(struct sc_uhid_devices *devices);
void
sc_uhid_devices_add_receiver(struct sc_uhid_devices *devices,
struct sc_uhid_receiver *receiver);
struct sc_uhid_receiver *
sc_uhid_devices_get_receiver(struct sc_uhid_devices *devices, uint16_t id);
#endif

View File

@ -5,6 +5,7 @@
#include "aoa_hid.h" #include "aoa_hid.h"
#include "util/log.h" #include "util/log.h"
#include "util/str.h"
// See <https://source.android.com/devices/accessories/aoa2#hid-support>. // See <https://source.android.com/devices/accessories/aoa2#hid-support>.
#define ACCESSORY_REGISTER_HID 54 #define ACCESSORY_REGISTER_HID 54
@ -14,37 +15,18 @@
#define DEFAULT_TIMEOUT 1000 #define DEFAULT_TIMEOUT 1000
#define SC_HID_EVENT_QUEUE_MAX 64 #define SC_AOA_EVENT_QUEUE_MAX 64
static void static void
sc_hid_event_log(const struct sc_hid_event *event) { sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) {
// HID Event: [00] FF FF FF FF... // HID Event: [00] FF FF FF FF...
assert(event->size); assert(event->size);
unsigned buffer_size = event->size * 3 + 1; char *hex = sc_str_to_hex_string(event->data, event->size);
char *buffer = malloc(buffer_size); if (!hex) {
if (!buffer) {
LOG_OOM();
return; return;
} }
for (unsigned i = 0; i < event->size; ++i) { LOGV("HID Event: [%d] %s", accessory_id, hex);
snprintf(buffer + i * 3, 4, " %02x", event->buffer[i]); free(hex);
}
LOGV("HID Event: [%d]%s", event->accessory_id, buffer);
free(buffer);
}
void
sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
unsigned char *buffer, uint16_t buffer_size) {
hid_event->accessory_id = accessory_id;
hid_event->buffer = buffer;
hid_event->size = buffer_size;
hid_event->ack_to_wait = SC_SEQUENCE_INVALID;
}
void
sc_hid_event_destroy(struct sc_hid_event *hid_event) {
free(hid_event->buffer);
} }
bool bool
@ -52,7 +34,7 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
struct sc_acksync *acksync) { struct sc_acksync *acksync) {
sc_vecdeque_init(&aoa->queue); sc_vecdeque_init(&aoa->queue);
if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) { if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) {
return false; return false;
} }
@ -76,12 +58,7 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
void void
sc_aoa_destroy(struct sc_aoa *aoa) { sc_aoa_destroy(struct sc_aoa *aoa) {
// Destroy remaining events sc_vecdeque_destroy(&aoa->queue);
while (!sc_vecdeque_is_empty(&aoa->queue)) {
struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue);
assert(event);
sc_hid_event_destroy(event);
}
sc_cond_destroy(&aoa->event_cond); sc_cond_destroy(&aoa->event_cond);
sc_mutex_destroy(&aoa->mutex); sc_mutex_destroy(&aoa->mutex);
@ -97,10 +74,10 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id,
// index (arg1): total length of the HID report descriptor // index (arg1): total length of the HID report descriptor
uint16_t value = accessory_id; uint16_t value = accessory_id;
uint16_t index = report_desc_size; uint16_t index = report_desc_size;
unsigned char *buffer = NULL; unsigned char *data = NULL;
uint16_t length = 0; uint16_t length = 0;
int result = libusb_control_transfer(aoa->usb->handle, request_type, int result = libusb_control_transfer(aoa->usb->handle, request_type,
request, value, index, buffer, length, request, value, index, data, length,
DEFAULT_TIMEOUT); DEFAULT_TIMEOUT);
if (result < 0) { if (result < 0) {
LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result)); LOGE("REGISTER_HID: libusb error: %s", libusb_strerror(result));
@ -113,7 +90,7 @@ sc_aoa_register_hid(struct sc_aoa *aoa, uint16_t accessory_id,
static bool static bool
sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, const uint8_t *report_desc,
uint16_t report_desc_size) { uint16_t report_desc_size) {
uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
uint8_t request = ACCESSORY_SET_HID_REPORT_DESC; uint8_t request = ACCESSORY_SET_HID_REPORT_DESC;
@ -130,14 +107,14 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
* See <https://libusb.sourceforge.io/api-1.0/libusb_packetoverflow.html> * See <https://libusb.sourceforge.io/api-1.0/libusb_packetoverflow.html>
*/ */
// value (arg0): accessory assigned ID for the HID device // value (arg0): accessory assigned ID for the HID device
// index (arg1): offset of data (buffer) in descriptor // index (arg1): offset of data in descriptor
uint16_t value = accessory_id; uint16_t value = accessory_id;
uint16_t index = 0; uint16_t index = 0;
// libusb_control_transfer expects a pointer to non-const // libusb_control_transfer expects a pointer to non-const
unsigned char *buffer = (unsigned char *) report_desc; unsigned char *data = (unsigned char *) report_desc;
uint16_t length = report_desc_size; uint16_t length = report_desc_size;
int result = libusb_control_transfer(aoa->usb->handle, request_type, int result = libusb_control_transfer(aoa->usb->handle, request_type,
request, value, index, buffer, length, request, value, index, data, length,
DEFAULT_TIMEOUT); DEFAULT_TIMEOUT);
if (result < 0) { if (result < 0) {
LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result)); LOGE("SET_HID_REPORT_DESC: libusb error: %s", libusb_strerror(result));
@ -150,7 +127,7 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
bool bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, uint16_t report_desc_size) { const uint8_t *report_desc, uint16_t report_desc_size) {
bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size); bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size);
if (!ok) { if (!ok) {
return false; return false;
@ -169,18 +146,19 @@ sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
} }
static bool static bool
sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id,
const struct sc_hid_event *event) {
uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
uint8_t request = ACCESSORY_SEND_HID_EVENT; uint8_t request = ACCESSORY_SEND_HID_EVENT;
// <https://source.android.com/devices/accessories/aoa2.html#hid-support> // <https://source.android.com/devices/accessories/aoa2.html#hid-support>
// value (arg0): accessory assigned ID for the HID device // value (arg0): accessory assigned ID for the HID device
// index (arg1): 0 (unused) // index (arg1): 0 (unused)
uint16_t value = event->accessory_id; uint16_t value = accessory_id;
uint16_t index = 0; uint16_t index = 0;
unsigned char *buffer = event->buffer; unsigned char *data = (uint8_t *) event->data; // discard const
uint16_t length = event->size; uint16_t length = event->size;
int result = libusb_control_transfer(aoa->usb->handle, request_type, int result = libusb_control_transfer(aoa->usb->handle, request_type,
request, value, index, buffer, length, request, value, index, data, length,
DEFAULT_TIMEOUT); DEFAULT_TIMEOUT);
if (result < 0) { if (result < 0) {
LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result)); LOGE("SEND_HID_EVENT: libusb error: %s", libusb_strerror(result));
@ -192,7 +170,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) {
} }
bool bool
sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) { sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) {
uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR; uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
uint8_t request = ACCESSORY_UNREGISTER_HID; uint8_t request = ACCESSORY_UNREGISTER_HID;
// <https://source.android.com/devices/accessories/aoa2.html#hid-support> // <https://source.android.com/devices/accessories/aoa2.html#hid-support>
@ -200,10 +178,10 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) {
// index (arg1): 0 // index (arg1): 0
uint16_t value = accessory_id; uint16_t value = accessory_id;
uint16_t index = 0; uint16_t index = 0;
unsigned char *buffer = NULL; unsigned char *data = NULL;
uint16_t length = 0; uint16_t length = 0;
int result = libusb_control_transfer(aoa->usb->handle, request_type, int result = libusb_control_transfer(aoa->usb->handle, request_type,
request, value, index, buffer, length, request, value, index, data, length,
DEFAULT_TIMEOUT); DEFAULT_TIMEOUT);
if (result < 0) { if (result < 0) {
LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result)); LOGE("UNREGISTER_HID: libusb error: %s", libusb_strerror(result));
@ -215,16 +193,25 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, const uint16_t accessory_id) {
} }
bool bool
sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) { sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa,
uint16_t accessory_id,
const struct sc_hid_event *event,
uint64_t ack_to_wait) {
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) { if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
sc_hid_event_log(event); sc_hid_event_log(accessory_id, event);
} }
sc_mutex_lock(&aoa->mutex); sc_mutex_lock(&aoa->mutex);
bool full = sc_vecdeque_is_full(&aoa->queue); bool full = sc_vecdeque_is_full(&aoa->queue);
if (!full) { if (!full) {
bool was_empty = sc_vecdeque_is_empty(&aoa->queue); bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
sc_vecdeque_push_noresize(&aoa->queue, *event);
struct sc_aoa_event *aoa_event =
sc_vecdeque_push_hole_noresize(&aoa->queue);
aoa_event->hid = *event;
aoa_event->accessory_id = accessory_id;
aoa_event->ack_to_wait = ack_to_wait;
if (was_empty) { if (was_empty) {
sc_cond_signal(&aoa->event_cond); sc_cond_signal(&aoa->event_cond);
} }
@ -252,7 +239,7 @@ run_aoa_thread(void *data) {
} }
assert(!sc_vecdeque_is_empty(&aoa->queue)); assert(!sc_vecdeque_is_empty(&aoa->queue));
struct sc_hid_event event = sc_vecdeque_pop(&aoa->queue); struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue);
uint64_t ack_to_wait = event.ack_to_wait; uint64_t ack_to_wait = event.ack_to_wait;
sc_mutex_unlock(&aoa->mutex); sc_mutex_unlock(&aoa->mutex);
@ -271,17 +258,14 @@ run_aoa_thread(void *data) {
if (result == SC_ACKSYNC_WAIT_TIMEOUT) { if (result == SC_ACKSYNC_WAIT_TIMEOUT) {
LOGW("Ack not received after 500ms, discarding HID event"); LOGW("Ack not received after 500ms, discarding HID event");
sc_hid_event_destroy(&event);
continue; continue;
} else if (result == SC_ACKSYNC_WAIT_INTR) { } else if (result == SC_ACKSYNC_WAIT_INTR) {
// stopped // stopped
sc_hid_event_destroy(&event);
break; break;
} }
} }
bool ok = sc_aoa_send_hid_event(aoa, &event); bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid);
sc_hid_event_destroy(&event);
if (!ok) { if (!ok) {
LOGW("Could not send HID event to USB device"); LOGW("Could not send HID event to USB device");
} }

View File

@ -6,28 +6,22 @@
#include <libusb-1.0/libusb.h> #include <libusb-1.0/libusb.h>
#include "hid/hid_event.h"
#include "usb.h" #include "usb.h"
#include "util/acksync.h" #include "util/acksync.h"
#include "util/thread.h" #include "util/thread.h"
#include "util/tick.h" #include "util/tick.h"
#include "util/vecdeque.h" #include "util/vecdeque.h"
struct sc_hid_event { #define SC_HID_MAX_SIZE 8
struct sc_aoa_event {
struct sc_hid_event hid;
uint16_t accessory_id; uint16_t accessory_id;
unsigned char *buffer;
uint16_t size;
uint64_t ack_to_wait; uint64_t ack_to_wait;
}; };
// Takes ownership of buffer struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event);
void
sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
unsigned char *buffer, uint16_t buffer_size);
void
sc_hid_event_destroy(struct sc_hid_event *hid_event);
struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event);
struct sc_aoa { struct sc_aoa {
struct sc_usb *usb; struct sc_usb *usb;
@ -35,7 +29,7 @@ struct sc_aoa {
sc_mutex mutex; sc_mutex mutex;
sc_cond event_cond; sc_cond event_cond;
bool stopped; bool stopped;
struct sc_hid_event_queue queue; struct sc_aoa_event_queue queue;
struct sc_acksync *acksync; struct sc_acksync *acksync;
}; };
@ -57,12 +51,22 @@ sc_aoa_join(struct sc_aoa *aoa);
bool bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id, sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const unsigned char *report_desc, uint16_t report_desc_size); const uint8_t *report_desc, uint16_t report_desc_size);
bool bool
sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id); sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id);
bool bool
sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event); sc_aoa_push_hid_event_with_ack_to_wait(struct sc_aoa *aoa,
uint16_t accessory_id,
const struct sc_hid_event *event,
uint64_t ack_to_wait);
static inline bool
sc_aoa_push_hid_event(struct sc_aoa *aoa, uint16_t accessory_id,
const struct sc_hid_event *event) {
return sc_aoa_push_hid_event_with_ack_to_wait(aoa, accessory_id, event,
SC_SEQUENCE_INVALID);
}
#endif #endif

View File

@ -1,267 +0,0 @@
#include "hid_mouse.h"
#include <assert.h>
#include "input_events.h"
#include "util/log.h"
/** Downcast mouse processor to hid_mouse */
#define DOWNCAST(MP) container_of(MP, struct sc_hid_mouse, mouse_processor)
#define HID_MOUSE_ACCESSORY_ID 2
// 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position
#define HID_MOUSE_EVENT_SIZE 4
/**
* Mouse descriptor from the specification:
* <https://www.usb.org/sites/default/files/hid1_11.pdf>
*
* Appendix E (p71): §E.10 Report Descriptor (Mouse)
*
* The usage tags (like Wheel) are listed in "HID Usage Tables":
* <https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf>
* §4 Generic Desktop Page (0x01) (p26)
*/
static const unsigned char mouse_report_desc[] = {
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Mouse)
0x09, 0x02,
// Collection (Application)
0xA1, 0x01,
// Usage (Pointer)
0x09, 0x01,
// Collection (Physical)
0xA1, 0x00,
// Usage Page (Buttons)
0x05, 0x09,
// Usage Minimum (1)
0x19, 0x01,
// Usage Maximum (5)
0x29, 0x05,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (1)
0x25, 0x01,
// Report Count (5)
0x95, 0x05,
// Report Size (1)
0x75, 0x01,
// Input (Data, Variable, Absolute): 5 buttons bits
0x81, 0x02,
// Report Count (1)
0x95, 0x01,
// Report Size (3)
0x75, 0x03,
// Input (Constant): 3 bits padding
0x81, 0x01,
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (X)
0x09, 0x30,
// Usage (Y)
0x09, 0x31,
// Usage (Wheel)
0x09, 0x38,
// Local Minimum (-127)
0x15, 0x81,
// Local Maximum (127)
0x25, 0x7F,
// Report Size (8)
0x75, 0x08,
// Report Count (3)
0x95, 0x03,
// Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel)
0x81, 0x06,
// End Collection
0xC0,
// End Collection
0xC0,
};
/**
* A mouse HID event is 3 bytes long:
*
* - byte 0: buttons state
* - byte 1: relative x motion (signed byte from -127 to 127)
* - byte 2: relative y motion (signed byte from -127 to 127)
*
* 7 6 5 4 3 2 1 0
* +---------------+
* byte 0: |0 0 0 . . . . .| buttons state
* +---------------+
* ^ ^ ^ ^ ^
* | | | | `- left button
* | | | `--- right button
* | | `----- middle button
* | `------- button 4
* `--------- button 5
*
* +---------------+
* byte 1: |. . . . . . . .| relative x motion
* +---------------+
* byte 2: |. . . . . . . .| relative y motion
* +---------------+
* byte 3: |. . . . . . . .| wheel motion (-1, 0 or 1)
* +---------------+
*
* As an example, here is the report for a motion of (x=5, y=-4) with left
* button pressed:
*
* +---------------+
* |0 0 0 0 0 0 0 1| left button pressed
* +---------------+
* |0 0 0 0 0 1 0 1| horizontal motion (x = 5)
* +---------------+
* |1 1 1 1 1 1 0 0| relative y motion (y = -4)
* +---------------+
* |0 0 0 0 0 0 0 0| wheel motion
* +---------------+
*/
static bool
sc_hid_mouse_event_init(struct sc_hid_event *hid_event) {
unsigned char *buffer = calloc(1, HID_MOUSE_EVENT_SIZE);
if (!buffer) {
LOG_OOM();
return false;
}
sc_hid_event_init(hid_event, HID_MOUSE_ACCESSORY_ID, buffer,
HID_MOUSE_EVENT_SIZE);
return true;
}
static unsigned char
buttons_state_to_hid_buttons(uint8_t buttons_state) {
unsigned char c = 0;
if (buttons_state & SC_MOUSE_BUTTON_LEFT) {
c |= 1 << 0;
}
if (buttons_state & SC_MOUSE_BUTTON_RIGHT) {
c |= 1 << 1;
}
if (buttons_state & SC_MOUSE_BUTTON_MIDDLE) {
c |= 1 << 2;
}
if (buttons_state & SC_MOUSE_BUTTON_X1) {
c |= 1 << 3;
}
if (buttons_state & SC_MOUSE_BUTTON_X2) {
c |= 1 << 4;
}
return c;
}
static void
sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
const struct sc_mouse_motion_event *event) {
struct sc_hid_mouse *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
if (!sc_hid_mouse_event_init(&hid_event)) {
return;
}
unsigned char *buffer = hid_event.buffer;
buffer[0] = buttons_state_to_hid_buttons(event->buttons_state);
buffer[1] = CLAMP(event->xrel, -127, 127);
buffer[2] = CLAMP(event->yrel, -127, 127);
buffer[3] = 0; // wheel coordinates only used for scrolling
if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) {
sc_hid_event_destroy(&hid_event);
LOGW("Could not request HID event (mouse motion)");
}
}
static void
sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
const struct sc_mouse_click_event *event) {
struct sc_hid_mouse *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
if (!sc_hid_mouse_event_init(&hid_event)) {
return;
}
unsigned char *buffer = hid_event.buffer;
buffer[0] = buttons_state_to_hid_buttons(event->buttons_state);
buffer[1] = 0; // no x motion
buffer[2] = 0; // no y motion
buffer[3] = 0; // wheel coordinates only used for scrolling
if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) {
sc_hid_event_destroy(&hid_event);
LOGW("Could not request HID event (mouse click)");
}
}
static void
sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
const struct sc_mouse_scroll_event *event) {
struct sc_hid_mouse *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
if (!sc_hid_mouse_event_init(&hid_event)) {
return;
}
unsigned char *buffer = hid_event.buffer;
buffer[0] = 0; // buttons state irrelevant (and unknown)
buffer[1] = 0; // no x motion
buffer[2] = 0; // no y motion
// In practice, vscroll is always -1, 0 or 1, but in theory other values
// are possible
buffer[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
if (!sc_aoa_push_hid_event(mouse->aoa, &hid_event)) {
sc_hid_event_destroy(&hid_event);
LOGW("Could not request HID event (mouse scroll)");
}
}
bool
sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa) {
mouse->aoa = aoa;
bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID, mouse_report_desc,
ARRAY_LEN(mouse_report_desc));
if (!ok) {
LOGW("Register HID mouse failed");
return false;
}
static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion,
.process_mouse_click = sc_mouse_processor_process_mouse_click,
.process_mouse_scroll = sc_mouse_processor_process_mouse_scroll,
// Touch events not supported (coordinates are not relative)
.process_touch = NULL,
};
mouse->mouse_processor.ops = &ops;
mouse->mouse_processor.relative_mode = true;
return true;
}
void
sc_hid_mouse_destroy(struct sc_hid_mouse *mouse) {
bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID);
if (!ok) {
LOGW("Could not unregister HID mouse");
}
}

110
app/src/usb/keyboard_aoa.c Normal file
View File

@ -0,0 +1,110 @@
#include "keyboard_aoa.h"
#include <assert.h>
#include "input_events.h"
#include "util/log.h"
/** Downcast key processor to keyboard_aoa */
#define DOWNCAST(KP) container_of(KP, struct sc_keyboard_aoa, key_processor)
#define HID_KEYBOARD_ACCESSORY_ID 1
static bool
push_mod_lock_state(struct sc_keyboard_aoa *kb, uint16_t mods_state) {
struct sc_hid_event hid_event;
if (!sc_hid_keyboard_event_from_mods(&hid_event, mods_state)) {
// Nothing to do
return true;
}
if (!sc_aoa_push_hid_event(kb->aoa, HID_KEYBOARD_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mod lock state)");
return false;
}
LOGD("HID keyboard state synchronized");
return true;
}
static void
sc_key_processor_process_key(struct sc_key_processor *kp,
const struct sc_key_event *event,
uint64_t ack_to_wait) {
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
return;
}
struct sc_keyboard_aoa *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
// Not all keys are supported, just ignore unsupported keys
if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) {
if (!kb->mod_lock_synchronized) {
// Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize
// keyboard state
if (push_mod_lock_state(kb, event->mods_state)) {
kb->mod_lock_synchronized = true;
}
}
// If ack_to_wait is != SC_SEQUENCE_INVALID, then Ctrl+v is pressed, so
// clipboard synchronization has been requested. Wait until clipboard
// synchronization is acknowledged by the server, otherwise it could
// paste the old clipboard content.
if (!sc_aoa_push_hid_event_with_ack_to_wait(kb->aoa,
HID_KEYBOARD_ACCESSORY_ID,
&hid_event,
ack_to_wait)) {
LOGW("Could not request HID event (key)");
}
}
}
bool
sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) {
kb->aoa = aoa;
bool ok = sc_aoa_setup_hid(aoa, HID_KEYBOARD_ACCESSORY_ID,
SC_HID_KEYBOARD_REPORT_DESC,
SC_HID_KEYBOARD_REPORT_DESC_LEN);
if (!ok) {
LOGW("Register HID keyboard failed");
return false;
}
sc_hid_keyboard_init(&kb->hid);
kb->mod_lock_synchronized = false;
static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key,
// Never forward text input via HID (all the keys are injected
// separately)
.process_text = NULL,
};
// Clipboard synchronization is requested over the control socket, while HID
// events are sent over AOA, so it must wait for clipboard synchronization
// to be acknowledged by the device before injecting Ctrl+v.
kb->key_processor.async_paste = true;
kb->key_processor.hid = true;
kb->key_processor.ops = &ops;
return true;
}
void
sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb) {
// Unregister HID keyboard so the soft keyboard shows again on Android
bool ok = sc_aoa_unregister_hid(kb->aoa, HID_KEYBOARD_ACCESSORY_ID);
if (!ok) {
LOGW("Could not unregister HID keyboard");
}
}

View File

@ -0,0 +1,27 @@
#ifndef SC_KEYBOARD_AOA_H
#define SC_KEYBOARD_AOA_H
#include "common.h"
#include <stdbool.h>
#include "aoa_hid.h"
#include "hid/hid_keyboard.h"
#include "trait/key_processor.h"
struct sc_keyboard_aoa {
struct sc_key_processor key_processor; // key processor trait
struct sc_hid_keyboard hid;
struct sc_aoa *aoa;
bool mod_lock_synchronized;
};
bool
sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa);
void
sc_keyboard_aoa_destroy(struct sc_keyboard_aoa *kb);
#endif

89
app/src/usb/mouse_aoa.c Normal file
View File

@ -0,0 +1,89 @@
#include "mouse_aoa.h"
#include <assert.h>
#include "hid/hid_mouse.h"
#include "input_events.h"
#include "util/log.h"
/** Downcast mouse processor to mouse_aoa */
#define DOWNCAST(MP) container_of(MP, struct sc_mouse_aoa, mouse_processor)
#define HID_MOUSE_ACCESSORY_ID 2
static void
sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
const struct sc_mouse_motion_event *event) {
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_motion(&hid_event, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse motion)");
}
}
static void
sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
const struct sc_mouse_click_event *event) {
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_click(&hid_event, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse click)");
}
}
static void
sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp,
const struct sc_mouse_scroll_event *event) {
struct sc_mouse_aoa *mouse = DOWNCAST(mp);
struct sc_hid_event hid_event;
sc_hid_mouse_event_from_scroll(&hid_event, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse scroll)");
}
}
bool
sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) {
mouse->aoa = aoa;
bool ok = sc_aoa_setup_hid(aoa, HID_MOUSE_ACCESSORY_ID,
SC_HID_MOUSE_REPORT_DESC,
SC_HID_MOUSE_REPORT_DESC_LEN);
if (!ok) {
LOGW("Register HID mouse failed");
return false;
}
static const struct sc_mouse_processor_ops ops = {
.process_mouse_motion = sc_mouse_processor_process_mouse_motion,
.process_mouse_click = sc_mouse_processor_process_mouse_click,
.process_mouse_scroll = sc_mouse_processor_process_mouse_scroll,
// Touch events not supported (coordinates are not relative)
.process_touch = NULL,
};
mouse->mouse_processor.ops = &ops;
mouse->mouse_processor.relative_mode = true;
return true;
}
void
sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse) {
bool ok = sc_aoa_unregister_hid(mouse->aoa, HID_MOUSE_ACCESSORY_ID);
if (!ok) {
LOGW("Could not unregister HID mouse");
}
}

View File

@ -1,5 +1,5 @@
#ifndef SC_HID_MOUSE_H #ifndef SC_MOUSE_AOA_H
#define SC_HID_MOUSE_H #define SC_MOUSE_AOA_H
#include "common.h" #include "common.h"
@ -8,16 +8,16 @@
#include "aoa_hid.h" #include "aoa_hid.h"
#include "trait/mouse_processor.h" #include "trait/mouse_processor.h"
struct sc_hid_mouse { struct sc_mouse_aoa {
struct sc_mouse_processor mouse_processor; // mouse processor trait struct sc_mouse_processor mouse_processor; // mouse processor trait
struct sc_aoa *aoa; struct sc_aoa *aoa;
}; };
bool bool
sc_hid_mouse_init(struct sc_hid_mouse *mouse, struct sc_aoa *aoa); sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa);
void void
sc_hid_mouse_destroy(struct sc_hid_mouse *mouse); sc_mouse_aoa_destroy(struct sc_mouse_aoa *mouse);
#endif #endif

View File

@ -10,8 +10,8 @@
struct scrcpy_otg { struct scrcpy_otg {
struct sc_usb usb; struct sc_usb usb;
struct sc_aoa aoa; struct sc_aoa aoa;
struct sc_hid_keyboard keyboard; struct sc_keyboard_aoa keyboard;
struct sc_hid_mouse mouse; struct sc_mouse_aoa mouse;
struct sc_screen_otg screen_otg; struct sc_screen_otg screen_otg;
}; };
@ -62,7 +62,7 @@ scrcpy_otg(struct scrcpy_options *options) {
// Minimal SDL initialization // Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) { if (SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError()); LOGE("Could not initialize SDL: %s", SDL_GetError());
return false; return SCRCPY_EXIT_FAILURE;
} }
atexit(SDL_Quit); atexit(SDL_Quit);
@ -73,8 +73,8 @@ scrcpy_otg(struct scrcpy_options *options) {
enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE; enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE;
struct sc_hid_keyboard *keyboard = NULL; struct sc_keyboard_aoa *keyboard = NULL;
struct sc_hid_mouse *mouse = NULL; struct sc_mouse_aoa *mouse = NULL;
bool usb_device_initialized = false; bool usb_device_initialized = false;
bool usb_connected = false; bool usb_connected = false;
bool aoa_started = false; bool aoa_started = false;
@ -117,19 +117,18 @@ scrcpy_otg(struct scrcpy_options *options) {
} }
aoa_initialized = true; aoa_initialized = true;
bool enable_keyboard = assert(options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA
options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID; || options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED);
bool enable_mouse = assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA
options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID; || options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED);
// If neither --hid-keyboard or --hid-mouse is passed, enable both bool enable_keyboard =
if (!enable_keyboard && !enable_mouse) { options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA;
enable_keyboard = true; bool enable_mouse =
enable_mouse = true; options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA;
}
if (enable_keyboard) { if (enable_keyboard) {
ok = sc_hid_keyboard_init(&s->keyboard, &s->aoa); ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa);
if (!ok) { if (!ok) {
goto end; goto end;
} }
@ -137,7 +136,7 @@ scrcpy_otg(struct scrcpy_options *options) {
} }
if (enable_mouse) { if (enable_mouse) {
ok = sc_hid_mouse_init(&s->mouse, &s->aoa); ok = sc_mouse_aoa_init(&s->mouse, &s->aoa);
if (!ok) { if (!ok) {
goto end; goto end;
} }
@ -186,10 +185,10 @@ end:
sc_usb_stop(&s->usb); sc_usb_stop(&s->usb);
if (mouse) { if (mouse) {
sc_hid_mouse_destroy(&s->mouse); sc_mouse_aoa_destroy(&s->mouse);
} }
if (keyboard) { if (keyboard) {
sc_hid_keyboard_destroy(&s->keyboard); sc_keyboard_aoa_destroy(&s->keyboard);
} }
if (aoa_initialized) { if (aoa_initialized) {

View File

@ -6,12 +6,12 @@
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "hid_keyboard.h" #include "keyboard_aoa.h"
#include "hid_mouse.h" #include "mouse_aoa.h"
struct sc_screen_otg { struct sc_screen_otg {
struct sc_hid_keyboard *keyboard; struct sc_keyboard_aoa *keyboard;
struct sc_hid_mouse *mouse; struct sc_mouse_aoa *mouse;
SDL_Window *window; SDL_Window *window;
SDL_Renderer *renderer; SDL_Renderer *renderer;
@ -22,8 +22,8 @@ struct sc_screen_otg {
}; };
struct sc_screen_otg_params { struct sc_screen_otg_params {
struct sc_hid_keyboard *keyboard; struct sc_keyboard_aoa *keyboard;
struct sc_hid_mouse *mouse; struct sc_mouse_aoa *mouse;
const char *window_title; const char *window_title;
bool always_on_top; bool always_on_top;

112
app/src/util/audiobuf.c Normal file
View File

@ -0,0 +1,112 @@
#include "audiobuf.h"
#include <stdlib.h>
#include <string.h>
#include <util/log.h>
#include <util/memory.h>
bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) {
assert(sample_size);
assert(capacity);
// The actual capacity is (alloc_size - 1) so that head == tail is
// non-ambiguous
buf->alloc_size = capacity + 1;
buf->data = sc_allocarray(buf->alloc_size, sample_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->sample_size = sample_size;
atomic_init(&buf->head, 0);
atomic_init(&buf->tail, 0);
return true;
}
void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
free(buf->data);
}
uint32_t
sc_audiobuf_read(struct sc_audiobuf *buf, void *to_, uint32_t samples_count) {
assert(samples_count);
uint8_t *to = to_;
// Only the reader thread can write tail without synchronization, so
// memory_order_relaxed is sufficient
uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_relaxed);
// The head cursor is updated after the data is written to the array
uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
uint32_t can_read = (buf->alloc_size + head - tail) % buf->alloc_size;
if (samples_count > can_read) {
samples_count = can_read;
}
if (to) {
uint32_t right_count = buf->alloc_size - tail;
if (right_count > samples_count) {
right_count = samples_count;
}
memcpy(to,
buf->data + (tail * buf->sample_size),
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(to + (right_count * buf->sample_size),
buf->data,
left_count * buf->sample_size);
}
}
uint32_t new_tail = (tail + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->tail, new_tail, memory_order_release);
return samples_count;
}
uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_,
uint32_t samples_count) {
const uint8_t *from = from_;
// Only the writer thread can write head, so memory_order_relaxed is
// sufficient
uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed);
// The tail cursor is updated after the data is consumed by the reader
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 (samples_count > can_write) {
samples_count = can_write;
}
uint32_t right_count = buf->alloc_size - head;
if (right_count > samples_count) {
right_count = samples_count;
}
memcpy(buf->data + (head * buf->sample_size),
from,
right_count * buf->sample_size);
if (samples_count > right_count) {
uint32_t left_count = samples_count - right_count;
memcpy(buf->data,
from + (right_count * buf->sample_size),
left_count * buf->sample_size);
}
uint32_t new_head = (head + samples_count) % buf->alloc_size;
atomic_store_explicit(&buf->head, new_head, memory_order_release);
return samples_count;
}

View File

@ -3,19 +3,25 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "util/bytebuf.h"
/** /**
* Wrapper around bytebuf to read and write samples * Wrapper around bytebuf to read and write samples
* *
* Each sample takes sample_size bytes. * Each sample takes sample_size bytes.
*/ */
struct sc_audiobuf { struct sc_audiobuf {
struct sc_bytebuf buf; uint8_t *data;
uint32_t alloc_size; // in samples
size_t sample_size; size_t sample_size;
atomic_uint_least32_t head; // writer cursor, in samples
atomic_uint_least32_t tail; // reader cursor, in samples
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
}; };
static inline uint32_t static inline uint32_t
@ -29,66 +35,31 @@ sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) {
return samples * buf->sample_size; return samples * buf->sample_size;
} }
static inline bool bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size, sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) { uint32_t capacity);
buf->sample_size = sample_size;
return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1);
}
static inline void void
sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) { sc_audiobuf_destroy(struct sc_audiobuf *buf);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_read(&buf->buf, to, bytes);
}
static inline void uint32_t
sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) { sc_audiobuf_read(struct sc_audiobuf *buf, void *to, uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_skip(&buf->buf, bytes);
}
static inline void uint32_t
sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_write(struct sc_audiobuf *buf, const void *from,
uint32_t samples) { uint32_t samples_count);
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_write(&buf->buf, from, bytes);
}
static inline void static inline uint32_t
sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from, sc_audiobuf_capacity(struct sc_audiobuf *buf) {
uint32_t samples) { assert(buf->alloc_size);
size_t bytes = sc_audiobuf_to_bytes(buf, samples); return buf->alloc_size - 1;
sc_bytebuf_prepare_write(&buf->buf, from, bytes);
}
static inline void
sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_commit_write(&buf->buf, bytes);
} }
static inline uint32_t static inline uint32_t
sc_audiobuf_can_read(struct sc_audiobuf *buf) { sc_audiobuf_can_read(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_read(&buf->buf); uint32_t head = atomic_load_explicit(&buf->head, memory_order_acquire);
return sc_audiobuf_to_samples(buf, bytes); uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire);
} return (buf->alloc_size + head - tail) % buf->alloc_size;
static inline uint32_t
sc_audiobuf_can_write(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_write(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline uint32_t
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_capacity(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
sc_bytebuf_destroy(&buf->buf);
} }
#endif #endif

View File

@ -1,104 +0,0 @@
#include "bytebuf.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "util/log.h"
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
assert(alloc_size);
buf->data = malloc(alloc_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->alloc_size = alloc_size;
buf->head = 0;
buf->tail = 0;
return true;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
free(buf->data);
}
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
size_t right_len = right_limit - buf->tail;
if (len < right_len) {
right_len = len;
}
memcpy(to, buf->data + buf->tail, right_len);
if (len > right_len) {
memcpy(to + right_len, buf->data, len - right_len);
}
buf->tail = (buf->tail + len) % buf->alloc_size;
}
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
buf->tail = (buf->tail + len) % buf->alloc_size;
}
static inline void
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
size_t right_len = buf->alloc_size - buf->head;
if (len < right_len) {
right_len = len;
}
memcpy(buf->data + buf->head, from, right_len);
if (len > right_len) {
memcpy(buf->data, from + right_len, len - right_len);
}
}
static inline void
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
buf->head = (buf->head + len) % buf->alloc_size;
}
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step0(buf, from, len);
sc_bytebuf_write_step1(buf, len);
}
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
// *This function MUST NOT access buf->tail (even in assert()).*
// The purpose of this function is to allow a reader and a writer to access
// different parts of the buffer in parallel simultaneously. It is intended
// to be called without lock (only sc_bytebuf_commit_write() is intended to
// be called with lock held).
assert(len < buf->alloc_size - 1);
sc_bytebuf_write_step0(buf, from, len);
}
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
assert(len <= sc_bytebuf_can_write(buf));
sc_bytebuf_write_step1(buf, len);
}

View File

@ -1,114 +0,0 @@
#ifndef SC_BYTEBUF_H
#define SC_BYTEBUF_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_bytebuf {
uint8_t *data;
// The actual capacity is (allocated - 1) so that head == tail is
// non-ambiguous
size_t alloc_size;
size_t head; // writter cursor
size_t tail; // reader cursor
// empty: tail == head
// full: ((tail + 1) % alloc_size) == head
};
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
/**
* Copy from the bytebuf to a user-provided array
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to read more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*/
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
/**
* Drop len bytes from the buffer
*
* The caller must check that len <= sc_bytebuf_read_available() (it is an
* error to attempt to skip more bytes than available).
*
* This function is guaranteed not to write to buf->head.
*
* It is equivalent to call sc_bytebuf_read() to some array and discard the
* array (but this function is more efficient since there is no copy).
*/
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
/**
* Copy the user-provided array to the bytebuf
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* This function is guaranteed not to write to buf->tail.
*/
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
/**
* Copy the user-provided array to the bytebuf, but do not advance the cursor
*
* The caller must check that len <= sc_bytebuf_write_available() (it is an
* error to write more bytes than the remaining available space).
*
* After this function is called, the write must be committed with
* sc_bytebuf_commit_write().
*
* The purpose of this mechanism is to acquire a lock only to commit the write,
* but not to perform the actual copy.
*
* This function is guaranteed not to access buf->tail.
*/
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len);
/**
* Commit a prepared write
*/
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
/**
* Return the number of bytes which can be read
*
* It is an error to read more bytes than available.
*/
static inline size_t
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
}
/**
* Return the number of bytes which can be written
*
* It is an error to write more bytes than available.
*/
static inline size_t
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
}
/**
* Return the actual capacity of the buffer (can_read() + can_write())
*/
static inline size_t
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
return buf->alloc_size - 1;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf);
#endif

View File

@ -3,6 +3,7 @@
#include <assert.h> #include <assert.h>
#include <errno.h> #include <errno.h>
#include <limits.h> #include <limits.h>
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@ -333,3 +334,22 @@ sc_str_remove_trailing_cr(char *s, size_t len) {
} }
return len; return len;
} }
char *
sc_str_to_hex_string(const uint8_t *data, size_t size) {
size_t buffer_size = size * 3 + 1;
char *buffer = malloc(buffer_size);
if (!buffer) {
LOG_OOM();
return NULL;
}
for (size_t i = 0; i < size; ++i) {
snprintf(buffer + i * 3, 4, "%02X ", data[i]);
}
// Remove the final space
buffer[size * 3] = '\0';
return buffer;
}

View File

@ -138,4 +138,10 @@ sc_str_index_of_column(const char *s, unsigned col, const char *seps);
size_t size_t
sc_str_remove_trailing_cr(char *s, size_t len); sc_str_remove_trailing_cr(char *s, size_t len);
/**
* Convert binary data to hexadecimal string
*/
char *
sc_str_to_hex_string(const uint8_t *data, size_t len);
#endif #endif

128
app/tests/test_audiobuf.c Normal file
View File

@ -0,0 +1,128 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/audiobuf.h"
static void test_audiobuf_simple(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5};
uint32_t w = sc_audiobuf_write(&buf, samples, 5);
assert(w == 5);
uint32_t r = sc_audiobuf_read(&buf, data, 4);
assert(r == 4);
assert(!memcmp(data, samples, 16));
uint32_t samples2[] = {6, 7, 8};
w = sc_audiobuf_write(&buf, samples2, 3);
assert(w == 3);
uint32_t single = 9;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
r = sc_audiobuf_read(&buf, &data[4], 8);
assert(r == 5);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
assert(!memcmp(data, expected, 36));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_boundaries(void) {
struct sc_audiobuf buf;
uint32_t data[20];
bool ok = sc_audiobuf_init(&buf, 4, 20);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
uint32_t r = sc_audiobuf_read(&buf, data, 9);
assert(r == 9);
uint32_t expected[] = {1, 2, 3, 4, 5, 6, 1, 2, 3};
assert(!memcmp(data, expected, 36));
uint32_t samples2[] = {7, 8, 9, 10, 11};
w = sc_audiobuf_write(&buf, samples2, 5);
assert(w == 5);
uint32_t single = 12;
w = sc_audiobuf_write(&buf, &single, 1);
assert(w == 1);
w = sc_audiobuf_read(&buf, NULL, 3);
assert(w == 3);
assert(sc_audiobuf_can_read(&buf) == 12);
r = sc_audiobuf_read(&buf, data, 12);
assert(r == 12);
uint32_t expected2[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
assert(!memcmp(data, expected2, 48));
sc_audiobuf_destroy(&buf);
}
static void test_audiobuf_partial_read_write(void) {
struct sc_audiobuf buf;
uint32_t data[15];
bool ok = sc_audiobuf_init(&buf, 4, 10);
assert(ok);
uint32_t samples[] = {1, 2, 3, 4, 5, 6};
uint32_t w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 6);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 4);
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 0);
uint32_t r = sc_audiobuf_read(&buf, data, 3);
assert(r == 3);
uint32_t expected[] = {1, 2, 3};
assert(!memcmp(data, expected, 12));
w = sc_audiobuf_write(&buf, samples, 6);
assert(w == 3);
r = sc_audiobuf_read(&buf, data, 15);
assert(r == 10);
uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3};
assert(!memcmp(data, expected2, 12));
sc_audiobuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_audiobuf_simple();
test_audiobuf_boundaries();
test_audiobuf_partial_read_write();
return 0;
}

View File

@ -1,126 +0,0 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/bytebuf.h"
static void test_bytebuf_simple(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
assert(sc_bytebuf_can_read(&buf) == 5);
sc_bytebuf_read(&buf, data, 4);
assert(!strncmp((char *) data, "hell", 4));
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
assert(sc_bytebuf_can_read(&buf) == 7);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 8);
sc_bytebuf_read(&buf, &data[4], 8);
assert(sc_bytebuf_can_read(&buf) == 0);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_boundaries(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 18);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 9));
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
static void test_bytebuf_two_steps_write(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 3));
assert(sc_bytebuf_can_read(&buf) == 3);
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 9);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_bytebuf_simple();
test_bytebuf_boundaries();
test_bytebuf_two_steps_write();
return 0;
}

View File

@ -1,6 +1,7 @@
#include "common.h" #include "common.h"
#include <assert.h> #include <assert.h>
#include <stdint.h>
#include <string.h> #include <string.h>
#include "control_msg.h" #include "control_msg.h"
@ -16,11 +17,11 @@ static void test_serialize_inject_keycode(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 14); assert(size == 14);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE, SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER 0x00, 0x00, 0x00, 0x42, // AKEYCODE_ENTER
@ -38,11 +39,11 @@ static void test_serialize_inject_text(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 18); assert(size == 18);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TEXT, SC_CONTROL_MSG_TYPE_INJECT_TEXT,
0x00, 0x00, 0x00, 0x0d, // text length 0x00, 0x00, 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
@ -58,11 +59,11 @@ static void test_serialize_inject_text_long(void) {
text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0'; text[SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH] = '\0';
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; uint8_t expected[5 + SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT; expected[0] = SC_CONTROL_MSG_TYPE_INJECT_TEXT;
expected[1] = 0x00; expected[1] = 0x00;
expected[2] = 0x00; expected[2] = 0x00;
@ -95,11 +96,11 @@ static void test_serialize_inject_touch_event(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 32); assert(size == 32);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT, SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
0x00, // AKEY_EVENT_ACTION_DOWN 0x00, // AKEY_EVENT_ACTION_DOWN
0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id 0x12, 0x34, 0x56, 0x78, 0x87, 0x65, 0x43, 0x21, // pointer id
@ -132,11 +133,11 @@ static void test_serialize_inject_scroll_event(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920 0x04, 0x38, 0x07, 0x80, // 1080 1920
@ -155,11 +156,11 @@ static void test_serialize_back_or_screen_on(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
0x01, // AKEY_EVENT_ACTION_UP 0x01, // AKEY_EVENT_ACTION_UP
}; };
@ -171,11 +172,11 @@ static void test_serialize_expand_notification_panel(void) {
.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, .type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -186,11 +187,11 @@ static void test_serialize_expand_settings_panel(void) {
.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, .type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL, SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -201,11 +202,11 @@ static void test_serialize_collapse_panels(void) {
.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, .type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
@ -219,11 +220,11 @@ static void test_serialize_get_clipboard(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
SC_COPY_KEY_COPY, SC_COPY_KEY_COPY,
}; };
@ -240,11 +241,11 @@ static void test_serialize_set_clipboard(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 27); assert(size == 27);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
1, // paste 1, // paste
@ -269,11 +270,11 @@ static void test_serialize_set_clipboard_long(void) {
text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0'; text[SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH] = '\0';
msg.set_clipboard.text = text; msg.set_clipboard.text = text;
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == SC_CONTROL_MSG_MAX_SIZE); assert(size == SC_CONTROL_MSG_MAX_SIZE);
unsigned char expected[SC_CONTROL_MSG_MAX_SIZE] = { uint8_t expected[SC_CONTROL_MSG_MAX_SIZE] = {
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
1, // paste 1, // paste
@ -296,11 +297,11 @@ static void test_serialize_set_screen_power_mode(void) {
}, },
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
0x02, // SC_SCREEN_POWER_MODE_NORMAL 0x02, // SC_SCREEN_POWER_MODE_NORMAL
}; };
@ -312,16 +313,78 @@ static void test_serialize_rotate_device(void) {
.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, .type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE]; uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf); size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_uhid_create(void) {
const uint8_t report_desc[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
.uhid_create = {
.id = 42,
.report_desc_size = sizeof(report_desc),
.report_desc = report_desc,
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 16);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_CREATE,
0, 42, // id
0, 11, // size
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_uhid_input(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_UHID_INPUT,
.uhid_input = {
.id = 42,
.size = 5,
.data = {1, 2, 3, 4, 5},
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 10);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_INPUT,
0, 42, // id
0, 5, // size
1, 2, 3, 4, 5,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_open_hard_keyboard(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 1);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -340,5 +403,8 @@ int main(int argc, char *argv[]) {
test_serialize_set_clipboard_long(); test_serialize_set_clipboard_long();
test_serialize_set_screen_power_mode(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_uhid_create();
test_serialize_uhid_input();
test_serialize_open_hard_keyboard();
return 0; return 0;
} }

View File

@ -1,32 +1,32 @@
#include "common.h" #include "common.h"
#include <assert.h> #include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include "device_msg.h" #include "device_msg.h"
#include <stdio.h>
static void test_deserialize_clipboard(void) { static void test_deserialize_clipboard(void) {
const unsigned char input[] = { const uint8_t input[] = {
DEVICE_MSG_TYPE_CLIPBOARD, DEVICE_MSG_TYPE_CLIPBOARD,
0x00, 0x00, 0x00, 0x03, // text length 0x00, 0x00, 0x00, 0x03, // text length
0x41, 0x42, 0x43, // "ABC" 0x41, 0x42, 0x43, // "ABC"
}; };
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 8); assert(r == 8);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
assert(msg.clipboard.text); assert(msg.clipboard.text);
assert(!strcmp("ABC", msg.clipboard.text)); assert(!strcmp("ABC", msg.clipboard.text));
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
} }
static void test_deserialize_clipboard_big(void) { static void test_deserialize_clipboard_big(void) {
unsigned char input[DEVICE_MSG_MAX_SIZE]; uint8_t input[DEVICE_MSG_MAX_SIZE];
input[0] = DEVICE_MSG_TYPE_CLIPBOARD; input[0] = DEVICE_MSG_TYPE_CLIPBOARD;
input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24; input[1] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0xff000000u) >> 24;
input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16; input[2] = (DEVICE_MSG_TEXT_MAX_LENGTH & 0x00ff0000u) >> 16;
@ -35,8 +35,8 @@ static void test_deserialize_clipboard_big(void) {
memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH); memset(input + 5, 'a', DEVICE_MSG_TEXT_MAX_LENGTH);
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == DEVICE_MSG_MAX_SIZE); assert(r == DEVICE_MSG_MAX_SIZE);
assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_CLIPBOARD);
@ -44,23 +44,45 @@ static void test_deserialize_clipboard_big(void) {
assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH); assert(strlen(msg.clipboard.text) == DEVICE_MSG_TEXT_MAX_LENGTH);
assert(msg.clipboard.text[0] == 'a'); assert(msg.clipboard.text[0] == 'a');
device_msg_destroy(&msg); sc_device_msg_destroy(&msg);
} }
static void test_deserialize_ack_set_clipboard(void) { static void test_deserialize_ack_set_clipboard(void) {
const unsigned char input[] = { const uint8_t input[] = {
DEVICE_MSG_TYPE_ACK_CLIPBOARD, DEVICE_MSG_TYPE_ACK_CLIPBOARD,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, // sequence
}; };
struct device_msg msg; struct sc_device_msg msg;
ssize_t r = device_msg_deserialize(input, sizeof(input), &msg); ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 9); assert(r == 9);
assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD); assert(msg.type == DEVICE_MSG_TYPE_ACK_CLIPBOARD);
assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708)); assert(msg.ack_clipboard.sequence == UINT64_C(0x0102030405060708));
} }
static void test_deserialize_uhid_output(void) {
const uint8_t input[] = {
DEVICE_MSG_TYPE_UHID_OUTPUT,
0, 42, // id
0, 5, // size
0x01, 0x02, 0x03, 0x04, 0x05, // data
};
struct sc_device_msg msg;
ssize_t r = sc_device_msg_deserialize(input, sizeof(input), &msg);
assert(r == 10);
assert(msg.type == DEVICE_MSG_TYPE_UHID_OUTPUT);
assert(msg.uhid_output.id == 42);
assert(msg.uhid_output.size == 5);
uint8_t expected[] = {1, 2, 3, 4, 5};
assert(!memcmp(msg.uhid_output.data, expected, sizeof(expected)));
sc_device_msg_destroy(&msg);
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -68,5 +90,6 @@ int main(int argc, char *argv[]) {
test_deserialize_clipboard(); test_deserialize_clipboard();
test_deserialize_clipboard_big(); test_deserialize_clipboard_big();
test_deserialize_ack_set_clipboard(); test_deserialize_ack_set_clipboard();
test_deserialize_uhid_output();
return 0; return 0;
} }

View File

@ -28,10 +28,17 @@ To disable only the audio playback, see [no playback](video.md#no-playback).
## Audio only ## Audio only
To play audio only, disable the video: To play audio only, disable video and control:
```bash ```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 # interrupt with Ctrl+C
``` ```

View File

@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v2.3.1`][direct-scrcpy-server] - [`scrcpy-server-v2.4`][direct-scrcpy-server]
<sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub> <sub>SHA-256: `93c272b7438605c055e127f7444064ed78fa9ca49f81156777fd201e79ce7ba3`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-server-v2.4
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

View File

@ -67,14 +67,6 @@ computer.
An option `--tcpip` allows to configure the connection automatically. There are An option `--tcpip` allows to configure the connection automatically. There are
two variants. two variants.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming _adb_ connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
address), connect the device over USB, then run: address), connect the device over USB, then run:
@ -85,6 +77,14 @@ scrcpy --tcpip # without arguments
It will automatically find the device IP address and adb port, enable TCP/IP It will automatically find the device IP address and adb port, enable TCP/IP
mode if necessary, then connect to the device before starting. mode if necessary, then connect to the device before starting.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming _adb_ connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
### Manual ### Manual

View File

@ -10,37 +10,20 @@ scrcpy --no-control
scrcpy -n # short version scrcpy -n # short version
``` ```
## Keyboard and mouse
## Text injection preference Read [keyboard](keyboard.md) and [mouse](mouse.md).
Two kinds of [events][textevents] are generated when typing text:
- _key events_, signaling that a key is pressed or released;
- _text events_, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves ## Control only
as expected in games (typically for WASD keys).
But this may [cause issues][prefertext]. If you encounter such a problem, you To control only with UHID mouse and keyboard:
can avoid it by:
```bash ```bash
scrcpy --prefer-text scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid
scrcpy --no-video --no-audio -KM # short version
``` ```
(but this will break keyboard behavior in games)
On the contrary, you could force to always inject raw key events:
```bash
scrcpy --raw-key-events
```
These options have no effect on HID keyboard (all key events are sent as
scancodes in this mode).
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
## Copy-paste ## Copy-paste
@ -85,7 +68,8 @@ way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`. `--no-clipboard-autosync`.
## Pinch-to-zoom
## Pinch-to-zoom, rotate and tilt simulation
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_. To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
@ -93,23 +77,18 @@ More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
Until the left-click button is released, all mouse movements scale and rotate Until the left-click button is released, all mouse movements scale and rotate
the content (if supported by the app) relative to the center of the screen. the content (if supported by the app) relative to the center of the screen.
https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767
To simulate a tilt gesture: <kbd>Shift</kbd>+_click-and-move-up-or-down_.
https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f
Technically, _scrcpy_ generates additional touch events from a "virtual finger" Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen. at a location inverted through the center of the screen. When pressing
<kbd>Ctrl</kbd> the _x_ and _y_ coordinates are inverted. Using <kbd>Shift</kbd>
only inverts _x_.
This only works for the default mouse mode (`--mouse=sdk`).
## Key repeat
By default, holding a key down generates repeated key events. This can cause
performance problems in some games, where these events are useless anyway.
To avoid forwarding repeated key events:
```bash
scrcpy --no-key-repeat
```
This option has no effect on HID keyboard (key repeat is handled by Android
directly in this mode).
## Right-click and middle-click ## Right-click and middle-click
@ -143,7 +122,3 @@ The target directory can be changed on start:
```bash ```bash
scrcpy --push-target=/sdcard/Movies/ scrcpy --push-target=/sdcard/Movies/
``` ```
## Physical keyboard and mouse simulation
See the dedicated [HID/OTG](hid-otg.md) page.

View File

@ -234,7 +234,7 @@ The video and audio streams are decoded by [FFmpeg].
The client parses the command line arguments, then [runs one of two code The client parses the command line arguments, then [runs one of two code
paths][run]: paths][run]:
- scrcpy in "normal" mode ([`scrcpy.c`]) - scrcpy in "normal" mode ([`scrcpy.c`])
- scrcpy in [OTG mode](hid-otg.md) ([`scrcpy_otg.c`]) - scrcpy in [OTG mode](otg.md) ([`scrcpy_otg.c`])
[run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82 [run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82
[`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293 [`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293

View File

@ -1,112 +0,0 @@
# HID/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.
A special [OTG](#otg) mode allows to control the device without mirroring (and
without USB debugging).
## Physical keyboard simulation
By default, _scrcpy_ uses Android key or text injection. It works everywhere,
but is limited to ASCII.
Instead, it can simulate a physical USB keyboard on Android to provide a better
input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard
is disabled and it works for all characters and IME.
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
However, it only works if the device is connected via USB.
Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it
is not possible to open a USB device if it is already open by another process
like the _adb daemon_).
To enable this mode:
```bash
scrcpy --hid-keyboard
scrcpy -K # short version
```
If it fails for some reason (for example because the device is not connected via
USB), it automatically fallbacks to the default mode (with a log in the
console). This allows using the same command line options when connected over
USB and TCP/IP.
In this mode, raw key events (scancodes) are sent to the device, independently
of the host key mapping. Therefore, if your keyboard layout does not match, it
must be configured on the Android device, in Settings → System → Languages and
input → [Physical keyboard].
This settings page can be started directly:
```bash
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
```
However, the option is only available when the HID keyboard is enabled (or when
a physical keyboard is connected).
[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
## Physical mouse simulation
By default, _scrcpy_ uses Android mouse events injection with absolute
coordinates. By simulating a physical mouse, a mouse pointer appears on the
Android device, and relative mouse motion, clicks and scrolls are injected.
To enable this mode:
```bash
scrcpy --hid-mouse
scrcpy -M # short version
```
When this mode is enabled, the computer mouse is "captured" (the mouse pointer
disappears from the computer and appears on the Android device instead).
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
(disable or enable) the mouse capture. Use one of them to give the control of
the mouse back to the computer.
## OTG
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.
In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled.
This is similar to `--hid-keyboard --hid-mouse`, but without mirroring.
To enable OTG mode:
```bash
scrcpy --otg
# Pass the serial if several USB devices are available
scrcpy --otg -s 0123456789abcdef
```
It is possible to enable only HID keyboard or HID mouse:
```bash
scrcpy --otg --hid-keyboard # keyboard only
scrcpy --otg --hid-mouse # mouse only
scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse
# for convenience, enable both by default
scrcpy --otg # keyboard and mouse
```
Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is
connected over USB.
## HID/OTG issues on Windows
See [FAQ](/FAQ.md#hidotg-issues-on-windows).

136
doc/keyboard.md Normal file
View File

@ -0,0 +1,136 @@
# Keyboard
Several keyboard input modes are available:
- `--keyboard=sdk` (default)
- `--keyboard=uhid` (or `-K`): simulates a physical HID keyboard using the UHID
kernel module on the device
- `--keyboard=aoa`: simulates a physical HID keyboard using the AOAv2 protocol
- `--keyboard=disabled`
By default, `sdk` is used, but if you use scrcpy regularly, it is recommended to
use [`uhid`](#uhid) and configure the keyboard layout once and for all.
## SDK keyboard
In this mode (`--keyboard=sdk`, or if the parameter is omitted), keyboard input
events are injected at the Android API level. It works everywhere, but it is
limited to ASCII and some other characters.
Note that on some devices, an additional option must be enabled in developer
options for this keyboard mode to work. See
[prerequisites](/README.md#prerequisites).
Additional parameters (specific to `--keyboard=sdk`) described below allow to
customize the behavior.
### Text injection preference
Two kinds of [events][textevents] are generated when typing text:
- _key events_, signaling that a key is pressed or released;
- _text events_, signaling that a text has been entered.
By default, numbers and "special characters" are inserted using text events, but
letters are injected using key events, so that the keyboard behaves as expected
in games (typically for WASD keys).
But this may [cause issues][prefertext]. If you encounter such a problem, you
can inject letters as text (or just switch to [UHID](#uhid)):
```bash
scrcpy --prefer-text
```
(but this will break keyboard behavior in games)
On the contrary, you could force to always inject raw key events:
```bash
scrcpy --raw-key-events
```
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
### Key repeat
By default, holding a key down generates repeated key events. Ths can cause
performance problems in some games, where these events are useless anyway.
To avoid forwarding repeated key events:
```bash
scrcpy --no-key-repeat
```
## Physical keyboard simulation
Two modes allow to simulate a physical HID keyboard on the device.
To work properly, it is necessary to configure (once and for all) the keyboard
layout on the device to match that of the computer.
The configuration page can be opened in one of the following ways:
- from the scrcpy window (when `uhid` or `aoa` is used), by pressing
<kbd>MOD</kbd>+<kbd>k</kbd> (see [shortcuts](shortcuts.md))
- from the device, in Settings → System → Languages and input → Physical
devices
- from a terminal on the computer, by executing `adb shell am start -a
android.settings.HARD_KEYBOARD_SETTINGS`
From this configuration page, it is also possible to enable or disable on-screen
keyboard.
### UHID
This mode simulates a physical HID keyboard using the [UHID] kernel module on the
device.
[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt
To enable UHID keyboard, use:
```bash
scrcpy --keyboard=uhid
scrcpy -K # short version
```
Once the keyboard layout is configured (see above), it is the best mode for
using the keyboard while mirroring:
- it works for all characters and IME (contrary to `--keyboard=sdk`)
- the on-screen keyboard can be disabled (contrary to `--keyboard=sdk`)
- it works over TCP/IP (wirelessly) (contrary to `--keyboard=aoa`)
- there are no issues on Windows (contrary to `--keyboard=aoa`)
One drawback is that it may not work on old Android versions due to permission
errors.
### AOA
This mode simulates a physical HID keyboard using the [AOAv2] protocol.
[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support
To enable AOA keyboard, use:
```bash
scrcpy --keyboard=aoa
```
Contrary to the other modes, it works at the USB level directly (so it only
works over USB).
It does not use the scrcpy server, and does not require `adb` (USB debugging).
Therefore, it is possible to control the device (but not mirror) even with USB
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_).

70
doc/mouse.md Normal file
View File

@ -0,0 +1,70 @@
# Mouse
Several mouse input modes are available:
- `--mouse=sdk` (default)
- `--mouse=uhid` (or `-M`): simulates a physical HID mouse using the UHID
kernel module on the device
- `--mouse=aoa`: simulates a physical HID mouse using the AOAv2 protocol
- `--mouse=disabled`
## SDK mouse
In this mode (`--mouse=sdk`, or if the parameter is omitted), mouse input events
are injected at the Android API level with absolute coordinates.
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).
## Physical mouse simulation
Two modes allow to simulate a physical HID mouse on the device.
In these modes, the computer mouse is "captured": the mouse pointer disappears
from the computer and appears on the Android device instead.
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
(disable or enable) the mouse capture. Use one of them to give the control of
the mouse back to the computer.
### UHID
This mode simulates a physical HID mouse using the [UHID] kernel module on the
device.
[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt
To enable UHID mouse, use:
```bash
scrcpy --mouse=uhid
scrcpy -M # short version
```
### AOA
This mode simulates a physical HID mouse using the [AOAv2] protocol.
[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support
To enable AOA mouse, use:
```bash
scrcpy --mouse=aoa
```
Contrary to the other modes, it works at the USB level directly (so it only
works over USB).
It does not use the scrcpy server, and does not require `adb` (USB debugging).
Therefore, it is possible to control the device (but not mirror) even with USB
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_).

58
doc/otg.md Normal file
View File

@ -0,0 +1,58 @@
# OTG
By default, _scrcpy_ injects input events at the Android API level. As an
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)).
[physical keyboard]: keyboard.md#physical-keyboard-simulation
[physical mouse]: physical-keyboard-simulation
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.
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:
```bash
scrcpy --otg
# Pass the serial if several USB devices are available
scrcpy --otg -s 0123456789abcdef
```
It is possible to disable keyboard or mouse:
```bash
scrcpy --otg --keyboard=disabled
scrcpy --otg --mouse=disabled
```
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 only 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 ## No playback
To disable playback while recording: To disable playback and control while recording:
```bash ```bash
scrcpy --no-playback --record=file.mp4 scrcpy --no-playback --no-control --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
``` ```
It is also possible to disable video and audio playback separately: 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 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 ## Time limit
To limit the recording time: To limit the recording time:

View File

@ -28,6 +28,8 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_ | Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_
| Flip display horizontally | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(left)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(right)_ | Flip display horizontally | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(left)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(right)_
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_ | Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Pause or re-pause display | <kbd>MOD</kbd>+<kbd>z</kbd>
| Unpause display | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>z</kbd>
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd> | Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_ | Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
@ -48,8 +50,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Cut to clipboard⁵ | <kbd>MOD</kbd>+<kbd>x</kbd> | Cut to clipboard⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd> | Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> | Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Open keyboard settings (HID keyboard only) | <kbd>MOD</kbd>+<kbd>k</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd> | Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_ | Pinch-to-zoom/rotate | <kbd>Ctrl</kbd>+_click-and-move_
| Tilt (slide vertically with 2 fingers) | <kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer | Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)

View File

@ -1,5 +1,14 @@
# Window # 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 ## Title
By default, the window title is the device model. It can be changed: By default, the window title is the device model. It can be changed:

View File

@ -4,14 +4,14 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.4.zip`][direct-win64] (64-bit)
<sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub> <sub>SHA-256: `9dc56f21bfa455352ec0c58b40feaf2fb02d67372910a4235e298ece286ff3a9`</sub>
- [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.4.zip`][direct-win32] (32-bit)
<sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub> <sub>SHA-256: `cf92acc45eef37c6ee2db819f92e420ced3bc50f1348dd57f7d6ca1fc80f6116`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip [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.3.1/scrcpy-win32-v2.3.1.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.4/scrcpy-win32-v2.4.zip
and extract it. and extract it.

View File

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

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '2.3.1', version: '2.4',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',
@ -16,5 +16,3 @@ endif
if get_option('compile_server') if get_option('compile_server')
subdir('server') subdir('server')
endif endif
run_target('run', command: ['scripts/run-scrcpy.sh'])

View File

@ -62,38 +62,38 @@ build-server:
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)" ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps: prepare-deps-win32:
@app/prebuilt-deps/prepare-adb.sh @app/deps/adb.sh win32
@app/prebuilt-deps/prepare-sdl.sh @app/deps/sdl.sh win32
@app/prebuilt-deps/prepare-ffmpeg.sh @app/deps/ffmpeg.sh win32
@app/prebuilt-deps/prepare-libusb.sh @app/deps/libusb.sh win32
build-win32: prepare-deps prepare-deps-win64:
@app/deps/adb.sh win64
@app/deps/sdl.sh win64
@app/deps/ffmpeg.sh win64
@app/deps/libusb.sh win64
build-win32: prepare-deps-win32
rm -rf "$(WIN32_BUILD_DIR)" rm -rf "$(WIN32_BUILD_DIR)"
mkdir -p "$(WIN32_BUILD_DIR)/local" mkdir -p "$(WIN32_BUILD_DIR)/local"
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/"
cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/"
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/"
meson setup "$(WIN32_BUILD_DIR)" \ meson setup "$(WIN32_BUILD_DIR)" \
--pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \
-Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \ -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \
-Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \ -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \
--cross-file=cross_win32.txt \ --cross-file=cross_win32.txt \
--buildtype=release --strip -Db_lto=true \ --buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \ -Dcompile_server=false \
-Dportable=true -Dportable=true
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps build-win64: prepare-deps-win64
rm -rf "$(WIN64_BUILD_DIR)" rm -rf "$(WIN64_BUILD_DIR)"
mkdir -p "$(WIN64_BUILD_DIR)/local" mkdir -p "$(WIN64_BUILD_DIR)/local"
cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/"
cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/"
cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/"
meson setup "$(WIN64_BUILD_DIR)" \ meson setup "$(WIN64_BUILD_DIR)" \
--pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \
-Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \ -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \
-Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \ -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \
--cross-file=cross_win64.txt \ --cross-file=cross_win64.txt \
--buildtype=release --strip -Db_lto=true \ --buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \ -Dcompile_server=false \
@ -108,10 +108,8 @@ dist-win32: build-server build-win32
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
@ -121,10 +119,8 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)"; \ cd "$(DIST)"; \

View File

@ -1,2 +0,0 @@
#!/usr/bin/env bash
SCRCPY_SERVER_PATH="$MESON_BUILD_ROOT/server/scrcpy-server" "$MESON_BUILD_ROOT/app/scrcpy"

View File

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

View File

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.3.1 SCRCPY_VERSION_NAME=2.4
PLATFORM=${ANDROID_PLATFORM:-34} PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}

View File

@ -79,7 +79,7 @@ public final class AudioCapture {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); ServiceManager.getActivityManager().startActivity(intent);
} }
private static void stopWorkaroundAndroid11() { private static void stopWorkaroundAndroid11() {
@ -153,13 +153,14 @@ public final class AudioCapture {
previousRecorderTimestamp = timestamp.nanoTime; previousRecorderTimestamp = timestamp.nanoTime;
} else { } else {
if (nextPts == 0) { if (nextPts == 0) {
Ln.w("Could not get any audio timestamp"); Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
} }
// compute from previous timestamp and packet size // compute from previous timestamp and packet size
pts = nextPts; pts = nextPts;
} }
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs; nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {

View File

@ -127,6 +127,10 @@ public class CameraCapture extends SurfaceCapture {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class); android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class);
if (sizes == null) {
return null;
}
Stream<android.util.Size> stream = Arrays.stream(sizes); Stream<android.util.Size> stream = Arrays.stream(sizes);
if (maxSize > 0) { if (maxSize > 0) {
stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize); stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize);

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