Compare commits

...

147 Commits

Author SHA1 Message Date
e39adb1b79 Detect codec/encoder mismatch
Fail with an explicit error when the requested encoder does not match
the requested codec.

Refs #5066 <https://github.com/Genymobile/scrcpy/issues/5066>
2024-09-20 08:18:42 +02:00
665ccb32f5 Update links to 2.7 2024-09-15 21:18:15 +02:00
292adf294d Bump version to 2.7 2024-09-15 18:59:27 +02:00
f9f3bfabe3 Merge branch 'master' into release 2024-09-15 18:59:16 +02:00
6d23a389ca Upgrade FFmpeg (7.0.2) for Windows 2024-09-15 18:58:53 +02:00
337901368e Upgrade SDL (2.30.7) for Windows 2024-09-15 18:58:53 +02:00
4cc4abdcc8 Mention issue with AOA and multiple gamepads
Android does not support multiple HID gamepads properly over AOA.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 18:58:53 +02:00
befc0fac5b Mention UHID permission errors
UHID may not work on old Android versions due to permission errors.

Mention it in UHID mouse and gamepad documentation (it was already
mentioned for UHID keyboard).

Refs #4473 comment <https://github.com/Genymobile/scrcpy/pull/4473#issuecomment-1975133226>
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 18:58:44 +02:00
f01a622ead Enable joystick events in background
Capture the gamepads even when the window is not focused.

Note: In theory, with this flag set, we could capture gamepad events
even without a window (--no-window). In practice, scrcpy still requires
a window, because --no-window implies --no-control, and the input
manager is owned by the sc_screen instance, which does not exist if
there is no window. Supporting this use case would require a lot of
refactors.

Refs <https://github.com/Genymobile/scrcpy/pull/5270#issuecomment-2339360460>
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>

Suggested-by: Luiz Henrique Laurini <luizhenriquelaurini@gmail.com>
2024-09-15 11:21:56 +02:00
0ba430a462 Add gamepad user documentation
Mainly copied and adapted from HID keyboard and mouse documentation.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
91d40c7548 Fix link in OTG documentation
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
9f3d51106d Remove fragile assert()
The sc_uhid_devices instance is initialized only when there is a UHID
keyboard.

The device message receiver assumed that it could not receive HID output
reports without a sc_uhid_devices instance (i.e. without a UHID
keyboard), but in practice, a UHID driver implementation on the device
may decide to send UHID output reports for mouse or for gamepads (and we
must just ignore them).

So remove the assert().

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
bf2b679e70 Simplify UHID outputs routing
There was a registration mechanism to listen to HID outputs with a
specific HID id.

However, the UHID gamepad processor handles several ids, so it cannot
work. We could complexify the registration mechanism, but instead,
directly dispatch to the expected processor based on the UHID id.

Concretely, instead of passing a sc_uhid_devices instance to construct a
sc_keyboard_uhid, so that it can register itself, construct the
sc_uhid_devices with all the UHID instances (currently only
sc_keyboard_uhid) so that it can dispatch HID outputs directly.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
7f250dd669 Mention physical gamepad names for UHID devices
Initialize UHID devices with a custom name:
 - "scrcpy: $GAMEPAD_NAME" for gamepads
 - "scrcpy" for keyboard and mouse (or if no gamepad name is available)

The name may appear in Android apps.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
68e27c7357 Reorder function parameters for consistency
Make the local function write_string() accept the output buffer as a
first parameter, like the other similar functions.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
c4febd55eb Make -K -M and -G use AOA in OTG mode
For convenience, short options were added to select UHID input modes:
 - -K for --keyboard=uhid
 - -M for --mouse=uhid
 - -G for --gamepad=uhid

In OTG mode, UHID is not available, so the short options should select
AOA instead.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
f9d1a333a0 Add UHID gamepad support
Similar to UHID keyboard and mouse, but for gamepads.

Can be enabled with --gamepad=uhid or -G.

It is not enabled by default because not all devices support UHID
(there is a permission error on old Android versions).

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
64a25f6e9d Add UHID_DESTROY control message
This message will be sent on gamepad disconnection.

Contrary to keyboard and mouse devices, which are registered once and
unregistered when scrcpy exists, each physical gamepad is mapped with
its own HID id, and they can be plugged and unplugged dynamically.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
5fe884276b Add gamepad support in OTG mode
Implement gamepad support for OTG.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
3e68244dd3 Add connected gamepads on start
Trigger SDL_CONTROLLERDEVICEADDED for all gamepads already connected
when scrcpy starts. We want to handle both the gamepads initially
connected and the gamepads connected while scrcpy is running.

This is not racy, because this event may not be trigged automatically
until SDL events are "pumped" (SDL_PumpEvents/SDL_WaitEvent).

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
a34a62ca4b Add AOA gamepad support
Similar to AOA keyboard and mouse, but for gamepads.

Can be enabled with --gamepad=aoa.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
a59c6df4b7 Implement HID gamepad
Implement the HID protocol for gamepads, that will be used in further
commits by the AOA and UHID gamepad processor implementations.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
f4d1e49ad9 Add util functions to write in little-endian
This will be helpful for writing HID values.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
4565f36ee6 Handle SDL gamepad events
Introduce a gamepad processor trait, similar to the keyboard processor
and mouse processor traits.

Handle gamepad events received from SDL, convert them to scrcpy-specific
gamepad events, and forward them to the gamepad processor.

Further commits will provide AOA and UHID implementations of the gamepad
processor trait.

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

Co-authored-by: Luiz Henrique Laurini <luizhenriquelaurini@gmail.com>
2024-09-15 11:21:56 +02:00
c8479fe8bf Discard unknown SDL events
Mouse and keyboard events with unknown button/keycode/scancode cannot be
handled properly. Discard them without forwarding them to the
keyboard or mouse processors.

This can happen for example if a more recent version of SDL introduces
new enum values.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
de8455400c Fix HID comments
Fix typo and reference the latest version of "HID Usage Tables"
specifications.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
1f5be743b4 Make AOA keyboard/mouse open error fatal
Now that the AOA open/close are asynchronous, an open error did not make
scrcpy exit anymore.

Add a mechanism to exit if the AOA device could not be opened
asynchronously.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
222916eebe Unregister all AOA devices automatically on exit
Pushing a close event from the keyboard_aoa or mouse_aoa implementation
was racy, because the AOA thread might be stopped before these events
were processed.

Instead, keep the list of open AOA devices to close them automatically
from the AOA thread before exiting.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
6c707ad8a3 Make HID logs uniform
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
d748ac75e6 Add AOA open/close verbose logs
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
6f0c9eba9b Introduce hid_open and hid_close events
This allows to handle HID open/close reports at the same place as HID
input reports (in the HID layer).

This will be especially useful to manage HID gamepads, to avoid
implementing one part in the HID layer and another part in the gamepad
processor implementation.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
f6219d2640 Rename hid_event to hid_input
The sc_hid_event structure represents HID input data. Rename it so that
we can add other hid event structs without confusion.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
6e9b0d7d4c Make AOA open and close asynchronous
For AOA keyboard and mouse, only input reports were asynchronous.
Register/unregister were called from the main thread.

This had the benefit to fail immediately if the AOA registration failed,
but we want to open/close AOA devices dynamically in order to add
gamepad support.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
3e9c89c535 Reorder AOA functions
This will allow sc_aoa_setup_hid() to compile even when
sc_aoa_unregister_hid() will be made static.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
9af3bacdd6 Refactor AOA handling
Extract event processing to a separate function.

This will make the code more readable when more event types will be
added.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
2dd02ebb80 Move HID ids to common HID code
The HID ids (accessory ids or UHID ids) were defined by the keyboard and
mouse implementations.

Instead, define them in the common HID part, and make that id part of
the sc_hid_event.

This prepares the introduction of gamepad support, which will handle
several gamepads (and ids) in the common HID gamepad code.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
dad04bf138 Fix HID mouse header guard
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
1afc8ca368 Add missing SC_ prefix for HID mouse event size
PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
785099b74d Remove duplicate definition SC_HID_MAX_SIZE
This constant is defined in hid_event.h.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
08da2e068e Fail on AOA keyboard/mouse initialization error
If the AOA keyboard or the AOA mouse fails to be initialized, this is a
fatal error.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
49c8ca34fd Introduce non-droppable control messages
Control messages are queued from the main thread and sent to the device
from a separate thread.

When the queue is full, messages are just dropped. This avoids to
accumulate too much delay between the client and the device in case of
network issue.

However, some messages should not be dropped: for example, dropping a
UHID_CREATE message would make all further UHID_INPUT messages invalid.
Therefore, mark these messages as non-droppable.

A non-droppable event is queued anyway (resizing the queue if
necessary, unless the allocation fails).

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
a84b0dfd0c Remove atomics from keyboard_uhid
The UHID output callback is now called from the same (main) thread as
the process_key() function.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
cbf5db85c1 Process UHID outputs events from the main thread
This will guarantee that the callbacks of UHID devices implementations
will always be called from the same (main) thread.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
72ee195693 Set clipboard from the main thread
The clipboard changes from the device are received from a separate
thread, but they must be handled from the main thread.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:56 +02:00
8620d06741 Add mechanism to execute code on the main thread
This allows to schedule a runnable to be executed on the main thread,
until the event loop is explicitly terminated.

It is guaranteed that all accepted runnables will be executed (this
avoids possible memory leaks if a runnable owns resources).

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-15 11:21:55 +02:00
e9240f6804 Expose main thread id
This will allow to assert that a function is called from the main
thread.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-14 21:24:15 +02:00
e9b32d8a52 Extract sc_push_event()
Expose a convenience function to push an event without args to the main
thread.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-14 21:24:15 +02:00
ce4e1fc420 Store events numbers in an enum
This avoids to manually set an explicit value for each item.

PR #5270 <https://github.com/Genymobile/scrcpy/pull/5270>
2024-09-14 21:24:15 +02:00
e8f02685e9 Fix deprecated references in scrcpy manpage
The options --hid-keyboard and --hid-mouse do not exist anymore. They
have been replaced by --keyboard=XXX and --mouse=XXX.
2024-09-14 21:24:15 +02:00
4a6b335f7d Do not send uninitialized HID event
If the function returns false, then there is nothing to send.
2024-09-14 21:24:15 +02:00
90ee0062cb Fix compilation with -Dusb=false
UHID does not depend on USB support, so the struct sc_uhid_devices must
always be defined.
2024-09-14 21:24:15 +02:00
e03888d587 Reject arguments containing new line characters
Refs bec3321fff
2024-09-14 21:23:44 +02:00
8453e3ba7d Enable TCP_NODELAY for the control socket
It is better to disable Nagle's algorithm to avoid unnecessary latency
for control messages. (I'm not sure this has any impact for a local TCP
socket though.)
2024-09-14 19:46:55 +02:00
145a9468fd Fix ifdef _WIN32
We use _WIN32 across the code base, not __WIN32.
2024-09-14 14:42:00 +02:00
1d713d7598 Do not parse --max-fps float in the client
Many parsing and formatting C functions like strtof() and asprintf() are
locale-dependent. Forcing a C locale just for the conversions in a way
that works on all platforms is a mess.

In practice, this is not a problem, scrcpy always uses the C locale,
because it never calls:

    setlocale(LC_ALL, "");

But the max-fps option should not depend on the locale configuration
anyway.

Since the value is parsed by the client in Java anyway, just forward the
string value as is.
2024-09-14 14:37:30 +02:00
265a15e0b1 Accept float values for --max-fps
Android accepts a float value, there is no reason to limit the option
to be an integer.

In particular, it allows to capture at a rate lower than 1 fps. For
example, to capture 1 frame every 5 seconds:

    scrcpy --video-source=camera --max-fps=0.2

It was already possible to pass a float manually:

    scrcpy --video-source=camera \
        --video-codec-options=max-fps-to-encoder:float=0.2

But accepting a float directly for --max-fps is more convenient.

Refs <https://developer.android.com/reference/android/media/MediaFormat#KEY_MAX_FPS_TO_ENCODER>
2024-09-13 22:02:25 +02:00
6451ad271a Ignore minBufferSize on error
A negative return value from AudioRecord.getMinBufferSize() represents
an error. Only consider positive values (0 would be invalid).

Refs #5228 <https://github.com/Genymobile/scrcpy/issues/5228>
2024-09-13 20:03:17 +02:00
bec3321fff Validate server arguments
Some command line arguments are passed as is to "adb shell". Therefore,
they must not contain special shell characters.
2024-09-13 19:53:05 +02:00
dea1fe3386 Validate crop and video size
A video width or height of 0 triggered an assert.

Fail explicitly instead: the server may actually send this size in
practice (for example on cropping with small dimensions, even if the
requested crop size is not 0).
2024-09-13 19:48:55 +02:00
a7cae59578 Improve delay buffer startup
The delay buffer clock estimates the clock offset between the PTS and
the frame decoded date using an "Exponentially Weighted Moving Average"
(EWMA).

But for the first frames, the clock have less than SC_CLOCK_RANGE
points to average. Since the timing for the first frames are typically
the worst ones, give more weight to the last point for the estimation.

Once SC_CLOCK_RANGE points are available (i.e. when SC_CLOCK_RANGE ==
clock->range), the new estimation is equivalent to the previous version.
2024-09-12 11:06:13 +02:00
f089ea67e1 Add missing flag initialization
The delay buffer `stopped` field was not initialized.

Since it practice the unique instance of sc_delay_buffer is initialized
in static memory, the flag was initialized to false as a side effect.

But with commit fd0f432e87, in debug mode
only, the delay buffer was broken.
2024-09-11 15:41:49 +02:00
63ced79842 Reverse NDEBUG conditions
By default, these specific debug logs are disabled.

Make the ifdef condition less confusing.
2024-09-11 15:41:29 +02:00
33a8c39beb Fix local NDEBUG define
The struct definition and the implementation did not use the same NDEBUG
constant.
2024-09-11 11:26:07 +02:00
903a5aaaf5 Replace "could not" by "cannot" where appropriate
"Could not" implies that the system tried to disable the option but
encountered an issue or failure.

"Cannot" indicates a rule or restriction, meaning it's not possible to
perform the action at all.
2024-09-09 08:24:52 +02:00
21b412cd98 Simplify messages reader/writer
In Java, control messages were parsed using manual buffering, which was
convoluted and error-prone.

Instead, read the socket directly through a DataInputStream and a
BufferedInputStream. Symmetrically, use a DataOutputStream and a
BufferedOutputStream to write messages.
2024-09-07 14:31:54 +02:00
3b241af3f6 Allow to pass an explicit version name on release
To build with a specific version name:

    VERSION=pr1234 ./release.sh

If not set, it will use the result of "git describe" (as before).
2024-09-06 23:07:15 +02:00
21e2e2606e Fix markdown formatting in documentation 2024-08-29 14:44:20 +02:00
0c95794463 Do not apply all workarounds for ONYX devices
Calling fillAppInfo() breaks video mirroring on ONYX devices.

Fixes #5182 <https://github.com/Genymobile/scrcpy/issues/5182>
Refs 2b6089cbfc
2024-08-10 14:29:26 +02:00
523f939532 Do not create UHID thread if not used
The HandlerThread is used only via the looper queue.
2024-08-08 20:32:35 +02:00
dd47cefa47 Fix typos
Refs <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1077968#27>
PR #5171 <https://github.com/Genymobile/scrcpy/pull/5171>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-08-05 16:13:02 +02:00
44b3fd82b1 Update links to 2.6.1 2024-08-02 22:58:09 +02:00
cc41115625 Bump version to 2.6.1 2024-08-02 22:32:04 +02:00
773c23fda2 Inject finger input whenever possible
Even if the pointer is a mouse, inject it as a finger unless it is
required to be a mouse, that is:
 - when it is a HOVER_MOUSE event, or
 - when a secondary button is pressed.

Some apps/games only accept events from a finger/touchscreen, so using a
mouse by default does not work for them.

For simplicity, make this change on the server side just before
event injection (so that the client does not need to know about this
hacky behavior).

Refs 6808288823
Refs c7b1d0ea9a
Fixes #5162 <https://github.com/Genymobile/scrcpy/issues/5162>
Fixes #5163 <https://github.com/Genymobile/scrcpy/issues/5163>
2024-08-02 22:24:31 +02:00
992b4922fe Document INJECT_EVENTS permission issue on Xiaomi
Make explicit in the prerequisites the exact error message when "USB
debugging (Security Settings)" is not set.
2024-08-02 18:44:42 +02:00
67f93350f6 Update links to 2.6 2024-08-01 18:46:10 +02:00
52136268ef Bump version to 2.6 2024-08-01 18:15:59 +02:00
0a6ccdc4df Merge branch 'master' into release 2024-08-01 18:15:40 +02:00
5d2441d198 Upgrade SDL (2.30.5) for Windows 2024-08-01 18:15:37 +02:00
2b6089cbfc Enable workarounds by default
Workarounds were disabled by default, and only enabled for some devices
or under specific conditions.

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

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

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

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

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

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

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

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

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

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

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

This allows to run:

    scrcpy --audio-dup

without specifying explicitly:

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

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

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

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

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

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

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

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

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

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

This will allow to provide another AudioCapture implementation.

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

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

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

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

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

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

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

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

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

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

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

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

Before:

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

After:

    release-v2.5-18-gc57a0512b

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

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

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

In addition to:

    --mouse-bind=xxxx

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    ERROR: Controller error

instead of:

    WARN: Device disconnected

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

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

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

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

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

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

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

It was deprecated in FFmpeg 7 by commit
1cc24d749569a42510399a29b034f7a77bdec34e:

<1cc24d7495>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Refs #4955 <https://github.com/Genymobile/scrcpy/issues/4955>
2024-05-29 10:32:58 +02:00
09e8c20168 Rename streamScreen() to streamCapture()
The capture source may be either the screen or the camera.
2024-05-14 08:23:57 +02:00
da484b7ab9 Reject recording with control only
If video and audio are disabled, there is nothing to record.
2024-05-12 10:44:27 +02:00
206809a99a Fix typo in documentation 2024-04-02 18:01:21 +02:00
172 changed files with 4486 additions and 1694 deletions

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

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

View File

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

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

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

View File

@ -2,7 +2,7 @@
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v2.4)
# scrcpy (v2.7)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@ -37,6 +37,7 @@ Its features include:
- [camera mirroring](doc/camera.md) (Android 12+)
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
- physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID)
- [gamepad](doc/gamepad.md) support
- [OTG mode](doc/otg.md)
- and more…
@ -53,10 +54,16 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable
On some devices, you also need to enable [an additional option][control] `USB
debugging (Security Settings)` (this is an item different from `USB debugging`)
to control it using a keyboard and mouse. Rebooting the device is necessary once
this option is set.
On some devices (especially Xiaomi), you might get the following error:
```
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
```
In that case, you need to enable [an additional option][control] `USB debugging
(Security Settings)` (this is an item different from `USB debugging`) to control
it using a keyboard and mouse. Rebooting the device is necessary once this
option is set.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
@ -105,6 +112,13 @@ Here are just some common examples.
scrcpy --otg
```
- Control the device using gamepad controllers plugged into the computer:
```bash
scrcpy --gamepad=uhid
scrcpy -G # short version
```
## User documentation
The application provides a lot of features and configuration options. They are
@ -116,6 +130,7 @@ documented in the following pages:
- [Control](doc/control.md)
- [Keyboard](doc/keyboard.md)
- [Mouse](doc/mouse.md)
- [Gamepad](doc/gamepad.md)
- [Device](doc/device.md)
- [Window](doc/window.md)
- [Recording](doc/recording.md)
@ -148,11 +163,14 @@ documented in the following pages:
## Contact
If you encounter a bug, please read the [FAQ](FAQ.md) first, then open an [issue].
You can open an [issue] for bug reports, feature requests or general questions.
For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution
to your problem immediately.
[issue]: https://github.com/Genymobile/scrcpy/issues
For general questions or discussions, you can also use:
You can also use:
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)

View File

@ -6,6 +6,7 @@ _scrcpy() {
--audio-buffer=
--audio-codec=
--audio-codec-options=
--audio-dup
--audio-encoder=
--audio-source=
--audio-output-buffer=
@ -25,7 +26,8 @@ _scrcpy() {
-e --select-tcpip
-f --fullscreen
--force-adb-forward
--forward-all-clicks
-G
--gamepad=
-h --help
-K
--keyboard=
@ -41,6 +43,7 @@ _scrcpy() {
-M
--max-fps=
--mouse=
--mouse-bind=
-n --no-control
-N --no-playback
--no-audio
@ -50,6 +53,7 @@ _scrcpy() {
--no-downsize-on-error
--no-key-repeat
--no-mipmaps
--no-mouse-hover
--no-power-on
--no-video
--no-video-playback
@ -110,7 +114,7 @@ _scrcpy() {
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
return
;;
--camera-facing)
@ -125,6 +129,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
return
;;
--gamepad)
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
return
;;
--orientation|--display-orientation)
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ src = [
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/events.c',
'src/icon.c',
'src/file_pusher.c',
'src/fps_counter.c',
@ -31,10 +32,12 @@ src = [
'src/screen.c',
'src/server.c',
'src/version.c',
'src/hid/hid_gamepad.c',
'src/hid/hid_keyboard.c',
'src/hid/hid_mouse.c',
'src/trait/frame_source.c',
'src/trait/packet_source.c',
'src/uhid/gamepad_uhid.c',
'src/uhid/keyboard_uhid.c',
'src/uhid/mouse_uhid.c',
'src/uhid/uhid_output.c',
@ -93,6 +96,7 @@ usb_support = get_option('usb')
if usb_support
src += [
'src/usb/aoa_hid.c',
'src/usb/gamepad_aoa.c',
'src/usb/keyboard_aoa.c',
'src/usb/mouse_aoa.c',
'src/usb/scrcpy_otg.c',

View File

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

View File

@ -29,7 +29,7 @@ Default is 128K (128000).
.BI "\-\-audio\-buffer " ms
Configure the audio buffering delay (in milliseconds).
Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches).
Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches).
Default is 50.
@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.B \-\-audio\-dup
Duplicate audio (capture and keep playing on the device).
This feature is only available with --audio-source=playback.
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
@ -57,7 +63,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Select the audio source (output, mic or playback).
The "output" source forwards the whole audio output, and disables playback on the device.
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
The "mic" source captures the microphone.
Default is output.
@ -164,16 +176,27 @@ Start in fullscreen.
Do not attempt to use "adb reverse" to connect to the device.
.TP
.B \-\-forward\-all\-clicks
By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead.
.B \-G
Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-gamepad " mode
Select how to send gamepad inputs to the device.
Possible values are "disabled", "uhid" and "aoa":
- "disabled" does not send gamepad inputs to the device.
- "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device.
- "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB.
Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR.
.TP
.B \-h, \-\-help
Print this help.
.TP
.B \-K
Same as \fB\-\-keyboard=uhid\fR.
Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-keyboard " mode
@ -192,7 +215,7 @@ For "uhid" and "aoa", the keyboard layout must be configured (once and for all)
This option is only available when the HID keyboard is enabled (or a physical keyboard is connected).
Also see \fB\-\-mouse\fR.
Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR.
.TP
.B \-\-kill\-adb\-on\-close
@ -238,7 +261,7 @@ Default is 0 (unlimited).
.TP
.B \-M
Same as \fB\-\-mouse=uhid\fR.
Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set.
.TP
.BI "\-\-max\-fps " value
@ -259,7 +282,28 @@ In "uhid" and "aoa" modes, the computer mouse is captured to control the device
LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer.
Also see \fB\-\-keyboard\fR.
Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR.
.TP
.BI "\-\-mouse\-bind " xxxx[:xxxx]
Configure bindings of secondary clicks.
The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click).
The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held.
If the second sequence of bindings is omitted, then it is the same as the first one.
Each character must be one of the following:
- '+': forward the click to the device
- '-': ignore the click
- 'b': trigger shortcut BACK (or turn screen on if off)
- 'h': trigger shortcut HOME
- 's': trigger shortcut APP_SWITCH
- 'n': trigger shortcut "expand notification panel"
Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID.
.TP
@ -304,6 +348,10 @@ Do not forward repeated key events when a key is held down.
.B \-\-no\-mipmaps
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
.TP
.B \-\-no\-mouse\-hover
Do not forward mouse hover (mouse motion without any clicks) events.
.TP
.B \-\-no\-power\-on
Do not power on the device on start.
@ -336,7 +384,7 @@ If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable ke
It may only work over USB.
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR.
.TP
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
@ -346,7 +394,7 @@ Default is 27183:27199.
.TP
\fB\-\-pause\-on\-exit\fR[=\fImode\fR]
Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured).
Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred).
This is useful to prevent the terminal window from automatically closing, so that error messages can be read.
@ -424,9 +472,9 @@ Turn the device screen off immediately.
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
Several shortcut modifiers can be specified, separated by ','.
For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctrl+lalt,lsuper".
For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper".
Default is "lalt,lsuper" (left-Alt or left-Super).

View File

@ -633,7 +633,7 @@ enum android_keycode {
* Toggles between BS and CS digital satellite services. */
AKEYCODE_TV_SATELLITE_SERVICE = 240,
/** Toggle Network key.
* Toggles selecting broacast services. */
* Toggles selecting broadcast services. */
AKEYCODE_TV_NETWORK = 241,
/** Antenna/Cable key.
* Toggles broadcast input source between antenna and cable. */

View File

@ -5,7 +5,7 @@
#include "util/log.h"
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug
/**
* Real-time audio player with configurable latency
@ -72,7 +72,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
size_t len = len_int;
uint32_t count = TO_SAMPLES(len);
#ifndef SC_AUDIO_PLAYER_NDEBUG
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
#endif
@ -162,7 +162,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples = MIN(ret, dst_nb_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
#endif
@ -194,7 +194,11 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// Still insufficient, drop old samples to make space
skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining);
assert(skipped_samples == remaining);
}
SDL_UnlockAudioDevice(ap->device);
if (written < samples) {
// Now there is enough space
uint32_t w = sc_audiobuf_write(&ap->buf,
swr_buf + TO_BYTES(written),
@ -202,8 +206,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
assert(w == remaining);
(void) w;
}
SDL_UnlockAudioDevice(ap->device);
}
uint32_t underflow = 0;
@ -242,7 +244,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
if (played) {
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG
#ifdef SC_AUDIO_PLAYER_DEBUG
} else {
LOGD("[Audio] Playback not started, skipping %" PRIu32
" samples", skip_samples);
@ -280,7 +282,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, can_read);
#ifndef SC_AUDIO_PLAYER_NDEBUG
#ifdef SC_AUDIO_PLAYER_DEBUG
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
can_read, sc_average_get(&ap->avg_buffering));
#endif

View File

@ -98,6 +98,10 @@ enum {
OPT_HID_KEYBOARD_DEPRECATED,
OPT_HID_MOUSE_DEPRECATED,
OPT_NO_WINDOW,
OPT_MOUSE_BIND,
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
OPT_GAMEPAD,
};
struct sc_option {
@ -153,7 +157,7 @@ static const struct sc_option options[] = {
.argdesc = "ms",
.text = "Configure the audio buffering delay (in milliseconds).\n"
"Lower values decrease the latency, but increase the "
"likelyhood of buffer underrun (causing audio glitches).\n"
"likelihood of buffer underrun (causing audio glitches).\n"
"Default is 50.",
},
{
@ -175,6 +179,13 @@ static const struct sc_option options[] = {
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_DUP,
.longopt = "audio-dup",
.text = "Duplicate audio (capture and keep playing on the device).\n"
"This feature is only available with --audio-source=playback."
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
@ -187,7 +198,13 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
.text = "Select the audio source (output, mic or playback).\n"
"The \"output\" source forwards the whole audio output, and "
"disables playback on the device.\n"
"The \"playback\" source captures the audio playback (Android "
"apps can opt-out, so the whole output is not necessarily "
"captured).\n"
"The \"mic\" source captures the microphone.\n"
"Default is output.",
},
{
@ -352,11 +369,26 @@ static const struct sc_option options[] = {
"device.",
},
{
// deprecated
.longopt_id = OPT_FORWARD_ALL_CLICKS,
.longopt = "forward-all-clicks",
.text = "By default, right-click triggers BACK (or POWER on) and "
"middle-click triggers HOME. This option disables these "
"shortcuts and forwards the clicks to the device instead.",
},
{
.shortopt = 'G',
.text = "Same as --gamepad=uhid, or --gamepad=aoa if --otg is set.",
},
{
.longopt_id = OPT_GAMEPAD,
.longopt = "gamepad",
.argdesc = "mode",
.text = "Select how to send gamepad inputs to the device.\n"
"Possible values are \"disabled\", \"uhid\" and \"aoa\".\n"
"\"disabled\" does not send gamepad inputs to the device.\n"
"\"uhid\" simulates physical HID gamepads using the Linux UHID "
"kernel module on the device.\n"
"\"aoa\" simulates physical gamepads using the AOAv2 protocol."
"It may only work over USB.\n"
"Also see --keyboard and --mouse.",
},
{
.shortopt = 'h',
@ -365,7 +397,7 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'K',
.text = "Same as --keyboard=uhid.",
.text = "Same as --keyboard=uhid, or --keyboard=aoa if --otg is set.",
},
{
.longopt_id = OPT_KEYBOARD,
@ -389,7 +421,7 @@ static const struct sc_option options[] = {
"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.",
"Also see --mouse and --gamepad.",
},
{
.longopt_id = OPT_KILL_ADB_ON_CLOSE,
@ -461,7 +493,7 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'M',
.text = "Same as --mouse=uhid.",
.text = "Same as --mouse=uhid, or --mouse=aoa if --otg is set.",
},
{
.longopt_id = OPT_MAX_FPS,
@ -488,7 +520,31 @@ static const struct sc_option options[] = {
"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.",
"Also see --keyboard and --gamepad.",
},
{
.longopt_id = OPT_MOUSE_BIND,
.longopt = "mouse-bind",
.argdesc = "xxxx[:xxxx]",
.text = "Configure bindings of secondary clicks.\n"
"The argument must be one or two sequences (separated by ':') "
"of exactly 4 characters, one for each secondary click (in "
"order: right click, middle click, 4th click, 5th click).\n"
"The first sequence defines the primary bindings, used when a "
"mouse button is pressed alone. The second sequence defines "
"the secondary bindings, used when a mouse button is pressed "
"while the Shift key is held.\n"
"If the second sequence of bindings is omitted, then it is the "
"same as the first one.\n"
"Each character must be one of the following:\n"
" '+': forward the click to the device\n"
" '-': ignore the click\n"
" 'b': trigger shortcut BACK (or turn screen on if off)\n"
" 'h': trigger shortcut HOME\n"
" 's': trigger shortcut APP_SWITCH\n"
" 'n': trigger shortcut \"expand notification panel\"\n"
"Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA "
"and UHID.",
},
{
.shortopt = 'n',
@ -552,6 +608,12 @@ static const struct sc_option options[] = {
"mipmaps are automatically generated to improve downscaling "
"quality. This option disables the generation of mipmaps.",
},
{
.longopt_id = OPT_NO_MOUSE_HOVER,
.longopt = "no-mouse-hover",
.text = "Do not forward mouse hover (mouse motion without any clicks) "
"events.",
},
{
.longopt_id = OPT_NO_POWER_ON,
.longopt = "no-power-on",
@ -593,7 +655,7 @@ static const struct sc_option options[] = {
"Keyboard and mouse may be disabled separately using"
"--keyboard=disabled and --mouse=disabled.\n"
"It may only work over USB.\n"
"See --keyboard and --mouse.",
"See --keyboard, --mouse and --gamepad.",
},
{
.shortopt = 'p',
@ -610,7 +672,7 @@ static const struct sc_option options[] = {
.optional_arg = true,
.text = "Configure pause on exit. Possible values are \"true\" (always "
"pause on exit), \"false\" (never pause on exit) and "
"\"if-error\" (pause only if an error occured).\n"
"\"if-error\" (pause only if an error occurred).\n"
"This is useful to prevent the terminal window from "
"automatically closing, so that error messages can be read.\n"
"Default is \"false\".\n"
@ -716,10 +778,10 @@ static const struct sc_option options[] = {
.text = "Specify the modifiers to use for scrcpy shortcuts.\n"
"Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", "
"\"lsuper\" and \"rsuper\".\n"
"A shortcut can consist in several keys, separated by '+'. "
"Several shortcuts can be specified, separated by ','.\n"
"For example, to use either LCtrl+LAlt or LSuper for scrcpy "
"shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"Several shortcut modifiers can be specified, separated by "
"','.\n"
"For example, to use either LCtrl or LSuper for scrcpy "
"shortcuts, pass \"lctrl,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
@ -1305,7 +1367,7 @@ print_exit_status(const struct sc_exit_status *status, unsigned cols) {
return;
}
assert(strlen(text) >= 9); // Contains at least the initial identation
assert(strlen(text) >= 9); // Contains at least the initial indentation
// text + 9 to remove the initial indentation
printf(" %3d %s\n", status->value, text + 9);
@ -1429,18 +1491,6 @@ parse_max_size(const char *s, uint16_t *max_size) {
return true;
}
static bool
parse_max_fps(const char *s, uint16_t *max_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps");
if (!ok) {
return false;
}
*max_fps = (uint16_t) value;
return true;
}
static bool
parse_buffering_time(const char *s, sc_tick *tick) {
long value;
@ -1687,82 +1737,62 @@ parse_log_level(const char *s, enum sc_log_level *log_level) {
return false;
}
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error)
static unsigned
static enum sc_shortcut_mod
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RSUPER;
} else {
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) key_len, item);
return 0;
}
if (STREQ("lctrl", item, len)) {
return SC_SHORTCUT_MOD_LCTRL;
}
if (STREQ("rctrl", item, len)) {
return SC_SHORTCUT_MOD_RCTRL;
}
if (STREQ("lalt", item, len)) {
return SC_SHORTCUT_MOD_LALT;
}
if (STREQ("ralt", item, len)) {
return SC_SHORTCUT_MOD_RALT;
}
if (STREQ("lsuper", item, len)) {
return SC_SHORTCUT_MOD_LSUPER;
}
if (STREQ("rsuper", item, len)) {
return SC_SHORTCUT_MOD_RSUPER;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
bool has_plus = strchr(item, '+');
if (has_plus) {
LOGE("Shortcut mod combination with '+' is not supported anymore: "
"'%.*s' (see #4741)", (int) len, item);
return 0;
}
return mod;
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) len, item);
return 0;
}
static bool
parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
parse_shortcut_mods(const char *s, uint8_t *shortcut_mods) {
uint8_t mods = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
// A list of shortcut modifiers, for example "lctrl,rctrl,rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
enum sc_shortcut_mod mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
mods |= mod;
if (!comma) {
break;
@ -1771,7 +1801,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
s = comma + 1;
}
mods->count = count;
*shortcut_mods = mods;
return true;
}
@ -1779,7 +1809,7 @@ parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
sc_parse_shortcut_mods(const char *s, uint8_t *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
@ -1921,7 +1951,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
if (!strcmp(optarg, "playback")) {
*source = SC_AUDIO_SOURCE_PLAYBACK;
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
optarg);
return false;
}
@ -2028,6 +2064,32 @@ parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) {
return false;
}
static bool
parse_gamepad(const char *optarg, enum sc_gamepad_input_mode *mode) {
if (!strcmp(optarg, "disabled")) {
*mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_GAMEPAD_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_GAMEPAD_INPUT_MODE_AOA;
return true;
#else
LOGE("--gamepad=aoa is disabled.");
return false;
#endif
}
LOGE("Unsupported gamepad: %s (expected disabled or aoa)", optarg);
return false;
}
static bool
parse_time_limit(const char *s, sc_tick *tick) {
long value;
@ -2058,11 +2120,85 @@ parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
}
LOGE("Unsupported pause on exit mode: %s "
"(expected true, false or if-error)", optarg);
"(expected true, false or if-error)", s);
return false;
}
static bool
parse_mouse_binding(char c, enum sc_mouse_binding *b) {
switch (c) {
case '+':
*b = SC_MOUSE_BINDING_CLICK;
return true;
case '-':
*b = SC_MOUSE_BINDING_DISABLED;
return true;
case 'b':
*b = SC_MOUSE_BINDING_BACK;
return true;
case 'h':
*b = SC_MOUSE_BINDING_HOME;
return true;
case 's':
*b = SC_MOUSE_BINDING_APP_SWITCH;
return true;
case 'n':
*b = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL;
return true;
default:
LOGE("Invalid mouse binding: '%c' "
"(expected '+', '-', 'b', 'h', 's' or 'n')", c);
return false;
}
}
static bool
parse_mouse_binding_set(const char *s, struct sc_mouse_binding_set *mbs) {
assert(strlen(s) >= 4);
if (!parse_mouse_binding(s[0], &mbs->right_click)) {
return false;
}
if (!parse_mouse_binding(s[1], &mbs->middle_click)) {
return false;
}
if (!parse_mouse_binding(s[2], &mbs->click4)) {
return false;
}
if (!parse_mouse_binding(s[3], &mbs->click5)) {
return false;
}
return true;
}
static bool
parse_mouse_bindings(const char *s, struct sc_mouse_bindings *mb) {
size_t len = strlen(s);
// either "xxxx" or "xxxx:xxxx"
if (len != 4 && (len != 9 || s[4] != ':')) {
LOGE("Invalid mouse bindings: '%s' (expected 'xxxx' or 'xxxx:xxxx', "
"with each 'x' being in {'+', '-', 'b', 'h', 's', 'n'})", s);
return false;
}
if (!parse_mouse_binding_set(s, &mb->pri)) {
return false;
}
if (len == 9) {
if (!parse_mouse_binding_set(s + 5, &mb->sec)) {
return false;
}
} else {
// use the same bindings for Shift+click
mb->sec = mb->pri;
}
return true;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@ -2116,7 +2252,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
args->help = true;
break;
case 'K':
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID;
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_KEYBOARD:
if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) {
@ -2128,9 +2264,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
"--keyboard=uhid instead.");
return false;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
opts->max_fps = optarg;
break;
case 'm':
if (!parse_max_size(optarg, &opts->max_size)) {
@ -2138,13 +2272,21 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case 'M':
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_MOUSE:
if (!parse_mouse(optarg, &opts->mouse_input_mode)) {
return false;
}
break;
case OPT_MOUSE_BIND:
if (!parse_mouse_bindings(optarg, &opts->mouse_bindings)) {
return false;
}
break;
case OPT_NO_MOUSE_HOVER:
opts->mouse_hover = false;
break;
case OPT_HID_MOUSE_DEPRECATED:
LOGE("--hid-mouse has been removed, use --mouse=aoa or "
"--mouse=uhid instead.");
@ -2342,7 +2484,22 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case OPT_FORWARD_ALL_CLICKS:
opts->forward_all_clicks = true;
LOGW("--forward-all-clicks is deprecated, "
"use --mouse-bind=++++ instead.");
opts->mouse_bindings = (struct sc_mouse_bindings) {
.pri = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
.sec = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
},
};
break;
case OPT_LEGACY_PASTE:
opts->legacy_paste = true;
@ -2496,6 +2653,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_WINDOW:
opts->window = false;
break;
case OPT_AUDIO_DUP:
opts->audio_dup = true;
break;
case 'G':
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA;
break;
case OPT_GAMEPAD:
if (!parse_gamepad(optarg, &opts->gamepad_input_mode)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;
@ -2613,7 +2781,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
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;
} else if (opts->keyboard_input_mode
== SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_UHID;
}
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
if (otg) {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
@ -2623,11 +2796,52 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
} else {
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
}
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID_OR_AOA) {
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
: SC_MOUSE_INPUT_MODE_UHID;
} else if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK
&& !opts->video_playback) {
LOGE("SDK mouse mode requires video playback. Try --mouse=uhid.");
return false;
}
if (opts->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA) {
opts->gamepad_input_mode = otg ? SC_GAMEPAD_INPUT_MODE_AOA
: SC_GAMEPAD_INPUT_MODE_UHID;
}
}
// If mouse bindings are not explicitly set, configure default bindings
if (opts->mouse_bindings.pri.right_click == SC_MOUSE_BINDING_AUTO) {
assert(opts->mouse_bindings.pri.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.pri.click5 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.right_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.middle_click == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click4 == SC_MOUSE_BINDING_AUTO);
assert(opts->mouse_bindings.sec.click5 == SC_MOUSE_BINDING_AUTO);
static struct sc_mouse_binding_set default_shortcuts = {
.right_click = SC_MOUSE_BINDING_BACK,
.middle_click = SC_MOUSE_BINDING_HOME,
.click4 = SC_MOUSE_BINDING_APP_SWITCH,
.click5 = SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
static struct sc_mouse_binding_set forward = {
.right_click = SC_MOUSE_BINDING_CLICK,
.middle_click = SC_MOUSE_BINDING_CLICK,
.click4 = SC_MOUSE_BINDING_CLICK,
.click5 = SC_MOUSE_BINDING_CLICK,
};
// By default, forward all clicks only for UHID and AOA
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
opts->mouse_bindings.pri = default_shortcuts;
opts->mouse_bindings.sec = forward;
} else {
opts->mouse_bindings.pri = forward;
opts->mouse_bindings.sec = default_shortcuts;
}
}
if (otg) {
@ -2650,9 +2864,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
enum sc_gamepad_input_mode gmode = opts->gamepad_input_mode;
if (gmode != SC_GAMEPAD_INPUT_MODE_AOA
&& gmode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --gamepad 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.");
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED
&& gmode == SC_GAMEPAD_INPUT_MODE_DISABLED) {
LOGE("Cannot not disable all inputs in OTG mode.");
return false;
}
}
@ -2674,6 +2896,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->mouse_input_mode != SC_MOUSE_INPUT_MODE_SDK
&& !opts->mouse_hover) {
LOGE("--no-mouse-over is specific to --mouse=sdk");
return false;
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
@ -2687,18 +2915,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
LOGE("Could not specify both --camera-id and --camera-facing");
LOGE("Cannot specify both --camera-id and --camera-facing");
return false;
}
if (opts->camera_size) {
if (opts->max_size) {
LOGE("Could not specify both --camera-size and -m/--max-size");
LOGE("Cannot specify both --camera-size and -m/--max-size");
return false;
}
if (opts->camera_ar) {
LOGE("Could not specify both --camera-size and --camera-ar");
LOGE("Cannot specify both --camera-size and --camera-ar");
return false;
}
}
@ -2725,19 +2953,42 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
if (opts->audio_dup) {
LOGI("Audio duplication enabled: audio source switched to "
"\"playback\"");
opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK;
} else {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
}
} else {
opts->audio_source = SC_AUDIO_SOURCE_MIC;
LOGI("Camera video source: microphone audio source selected");
}
}
if (opts->audio_dup) {
if (!opts->audio) {
LOGE("--audio-dup not supported if audio is disabled");
return false;
}
if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) {
LOGE("--audio-dup is specific to --audio-source=playback");
return false;
}
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (opts->record_filename) {
if (!opts->video && !opts->audio) {
LOGE("Video and audio disabled, nothing to record");
return false;
}
if (!opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
@ -2816,19 +3067,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (!opts->control) {
if (opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
LOGE("Cannot request to turn screen off if control is disabled");
return false;
}
if (opts->stay_awake) {
LOGE("Could not request to stay awake if control is disabled");
LOGE("Cannot request to stay awake if control is disabled");
return false;
}
if (opts->show_touches) {
LOGE("Could not request to show touches if control is disabled");
LOGE("Cannot request to show touches if control is disabled");
return false;
}
if (opts->power_off_on_close) {
LOGE("Could not request power off on close if control is disabled");
LOGE("Cannot request power off on close if control is disabled");
return false;
}
}
@ -2853,7 +3104,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
// OTG mode is compatible with only very few options.
// Only report obvious errors.
if (opts->record_filename) {
LOGE("OTG mode: could not record");
LOGE("OTG mode: cannot record");
return false;
}
if (opts->turn_screen_off) {
@ -2908,7 +3159,7 @@ sc_get_pause_on_exit(int argc, char *argv[]) {
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
// Set to false, inclusing when the value is invalid
// Set to false, including when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
}
}

View File

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

View File

@ -4,7 +4,7 @@
#include "util/log.h"
#define SC_CLOCK_NDEBUG // comment to debug
//#define SC_CLOCK_DEBUG // uncomment to debug
#define SC_CLOCK_RANGE 32
@ -21,10 +21,12 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
}
sc_tick offset = system - stream;
clock->offset = ((clock->range - 1) * clock->offset + offset)
/ clock->range;
unsigned clock_weight = clock->range - 1;
unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1;
clock->offset = (clock->offset * clock_weight + offset * value_weight)
/ SC_CLOCK_RANGE;
#ifndef SC_CLOCK_NDEBUG
#ifdef SC_CLOCK_DEBUG
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
#endif
}

View File

@ -8,7 +8,7 @@
#include <libavutil/version.h>
#include <SDL2/SDL_version.h>
#ifndef __WIN32
#ifndef _WIN32
# define PRIu64_ PRIu64
# define SC_PRIsizet "zu"
#else

View File

@ -64,13 +64,11 @@ static const char *const copy_key_labels[] = {
static inline const char *
get_well_known_pointer_id_name(uint64_t pointer_id) {
switch (pointer_id) {
case POINTER_ID_MOUSE:
case SC_POINTER_ID_MOUSE:
return "mouse";
case POINTER_ID_GENERIC_FINGER:
case SC_POINTER_ID_GENERIC_FINGER:
return "finger";
case POINTER_ID_VIRTUAL_MOUSE:
return "vmouse";
case POINTER_ID_VIRTUAL_FINGER:
case SC_POINTER_ID_VIRTUAL_FINGER:
return "vfinger";
default:
return NULL;
@ -85,15 +83,34 @@ write_position(uint8_t *buf, const struct sc_position *position) {
sc_write16be(&buf[10], position->screen_size.height);
}
// write length (4 bytes) + string (non null-terminated)
// Write truncated string, and return the size
static size_t
write_string(const char *utf8, size_t max_len, uint8_t *buf) {
write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) {
if (!utf8) {
return 0;
}
size_t len = sc_str_utf8_truncation_index(utf8, max_len);
memcpy(payload, utf8, len);
return len;
}
// Write length (4 bytes) + string (non null-terminated)
static size_t
write_string(uint8_t *buf, const char *utf8, size_t max_len) {
size_t len = write_string_payload(buf + 4, utf8, max_len);
sc_write32be(buf, len);
memcpy(&buf[4], utf8, len);
return 4 + len;
}
// Write length (1 byte) + string (non null-terminated)
static size_t
write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) {
assert(max_len <= 0xFF);
size_t len = write_string_payload(buf + 1, utf8, max_len);
buf[0] = len;
return 1 + len;
}
size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
buf[0] = msg->type;
@ -105,9 +122,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
sc_write32be(&buf[10], msg->inject_keycode.metastate);
return 14;
case SC_CONTROL_MSG_TYPE_INJECT_TEXT: {
size_t len =
write_string(msg->inject_text.text,
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH, &buf[1]);
size_t len = write_string(&buf[1], msg->inject_text.text,
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
return 1 + len;
}
case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
@ -139,24 +155,34 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
sc_write64be(&buf[1], msg->set_clipboard.sequence);
buf[9] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH,
&buf[10]);
size_t len = write_string(&buf[10], msg->set_clipboard.text,
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
return 10 + len;
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
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;
size_t index = 3;
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
index += 2;
memcpy(&buf[index], msg->uhid_create.report_desc,
msg->uhid_create.report_desc_size);
index += msg->uhid_create.report_desc_size;
return index;
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_UHID_DESTROY:
sc_write16be(&buf[1], msg->uhid_destroy.id);
return 3;
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
@ -254,10 +280,15 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
LOG_CMSG("rotate device");
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);
case SC_CONTROL_MSG_TYPE_UHID_CREATE: {
// Quote only if name is not null
const char *name = msg->uhid_create.name;
const char *quote = name ? "\"" : "";
LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s "
"report_desc_size=%" PRIu16, msg->uhid_create.id,
quote, name, quote, 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);
@ -271,6 +302,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
}
break;
}
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id);
break;
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
LOG_CMSG("open hard keyboard settings");
break;
@ -280,6 +314,16 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
}
}
bool
sc_control_msg_is_droppable(const struct sc_control_msg *msg) {
// Cannot drop UHID_CREATE messages, because it would cause all further
// UHID_INPUT messages for this device to be invalid.
// Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE
// with the same id may fail.
return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE
&& msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY;
}
void
sc_control_msg_destroy(struct sc_control_msg *msg) {
switch (msg->type) {

View File

@ -18,12 +18,11 @@
// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
#define POINTER_ID_MOUSE UINT64_C(-1)
#define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
#define SC_POINTER_ID_MOUSE UINT64_C(-1)
#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2)
// Used for injecting an additional virtual pointer for pinch-to-zoom
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3)
enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
@ -40,6 +39,7 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
SC_CONTROL_MSG_TYPE_UHID_CREATE,
SC_CONTROL_MSG_TYPE_UHID_INPUT,
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
};
@ -98,6 +98,7 @@ struct sc_control_msg {
} set_screen_power_mode;
struct {
uint16_t id;
const char *name; // pointer to static data
uint16_t report_desc_size;
const uint8_t *report_desc; // pointer to static data
} uhid_create;
@ -106,6 +107,9 @@ struct sc_control_msg {
uint16_t size;
uint8_t data[SC_HID_MAX_SIZE];
} uhid_input;
struct {
uint16_t id;
} uhid_destroy;
};
};
@ -117,6 +121,11 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf);
void
sc_control_msg_log(const struct sc_control_msg *msg);
// Even when the buffer is "full", some messages must absolutely not be dropped
// to avoid inconsistencies.
bool
sc_control_msg_is_droppable(const struct sc_control_msg *msg);
void
sc_control_msg_destroy(struct sc_control_msg *msg);

View File

@ -4,15 +4,17 @@
#include "util/log.h"
#define SC_CONTROL_MSG_QUEUE_MAX 64
// Drop droppable events above this limit
#define SC_CONTROL_MSG_QUEUE_LIMIT 60
static void
sc_controller_receiver_on_error(struct sc_receiver *receiver, void *userdata) {
sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error,
void *userdata) {
(void) receiver;
struct sc_controller *controller = userdata;
// Forward the event to the controller listener
controller->cbs->on_error(controller, controller->cbs_userdata);
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
}
bool
@ -21,13 +23,15 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
void *cbs_userdata) {
sc_vecdeque_init(&controller->queue);
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
// Add 4 to support 4 non-droppable events without re-allocation
bool ok = sc_vecdeque_reserve(&controller->queue,
SC_CONTROL_MSG_QUEUE_LIMIT + 4);
if (!ok) {
return false;
}
static const struct sc_receiver_callbacks receiver_cbs = {
.on_error = sc_controller_receiver_on_error,
.on_ended = sc_controller_receiver_on_ended,
};
ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs,
@ -55,7 +59,7 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
controller->control_socket = control_socket;
controller->stopped = false;
assert(cbs && cbs->on_error);
assert(cbs && cbs->on_ended);
controller->cbs = cbs;
controller->cbs_userdata = cbs_userdata;
@ -92,39 +96,59 @@ sc_controller_push_msg(struct sc_controller *controller,
sc_control_msg_log(msg);
}
bool pushed = false;
sc_mutex_lock(&controller->mutex);
bool full = sc_vecdeque_is_full(&controller->queue);
if (!full) {
size_t size = sc_vecdeque_size(&controller->queue);
if (size < SC_CONTROL_MSG_QUEUE_LIMIT) {
bool was_empty = sc_vecdeque_is_empty(&controller->queue);
sc_vecdeque_push_noresize(&controller->queue, *msg);
pushed = true;
if (was_empty) {
sc_cond_signal(&controller->msg_cond);
}
} else if (!sc_control_msg_is_droppable(msg)) {
bool ok = sc_vecdeque_push(&controller->queue, *msg);
if (ok) {
pushed = true;
} else {
// A non-droppable event must be dropped anyway
LOG_OOM();
}
}
// Otherwise (if the queue is full), the msg is discarded
// Otherwise, the msg is discarded
sc_mutex_unlock(&controller->mutex);
return !full;
return pushed;
}
static bool
process_msg(struct sc_controller *controller,
const struct sc_control_msg *msg) {
const struct sc_control_msg *msg, bool *eos) {
static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
size_t length = sc_control_msg_serialize(msg, serialized_msg);
if (!length) {
*eos = false;
return false;
}
ssize_t w =
net_send_all(controller->control_socket, serialized_msg, length);
return (size_t) w == length;
if ((size_t) w != length) {
*eos = true;
return false;
}
return true;
}
static int
run_controller(void *data) {
struct sc_controller *controller = data;
bool error = false;
for (;;) {
sc_mutex_lock(&controller->mutex);
while (!controller->stopped
@ -134,6 +158,7 @@ run_controller(void *data) {
if (controller->stopped) {
// stop immediately, do not process further msgs
sc_mutex_unlock(&controller->mutex);
LOGD("Controller stopped");
break;
}
@ -141,20 +166,21 @@ run_controller(void *data) {
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
sc_mutex_unlock(&controller->mutex);
bool ok = process_msg(controller, &msg);
bool eos;
bool ok = process_msg(controller, &msg, &eos);
sc_control_msg_destroy(&msg);
if (!ok) {
LOGD("Could not write msg to socket");
goto error;
if (eos) {
LOGD("Controller stopped (socket closed)");
} // else error already logged
error = !eos;
break;
}
}
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
return 0;
error:
controller->cbs->on_error(controller, controller->cbs_userdata);
return 1; // ignored
}
bool

View File

@ -28,7 +28,8 @@ struct sc_controller {
};
struct sc_controller_callbacks {
void (*on_error)(struct sc_controller *controller, void *userdata);
void (*on_ended)(struct sc_controller *controller, bool error,
void *userdata);
};
bool

View File

@ -8,8 +8,6 @@
#include "util/log.h"
#define SC_BUFFERING_NDEBUG // comment to debug
/** Downcast frame_sink to sc_delay_buffer */
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
@ -80,7 +78,7 @@ run_buffering(void *data) {
goto stopped;
}
#ifndef SC_BUFFERING_NDEBUG
#ifdef SC_BUFFERING_DEBUG
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
pts, dframe.push_date, sc_tick_now());
#endif
@ -134,6 +132,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
sc_clock_init(&db->clock);
sc_vecdeque_init(&db->queue);
db->stopped = false;
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
goto error_destroy_wait_cond;
@ -206,7 +205,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
return false;
}
#ifndef SC_BUFFERING_NDEBUG
#ifdef SC_BUFFERING_DEBUG
dframe.push_date = sc_tick_now();
#endif

View File

@ -12,12 +12,14 @@
#include "util/tick.h"
#include "util/vecdeque.h"
//#define SC_BUFFERING_DEBUG // uncomment to debug
// forward declarations
typedef struct AVFrame AVFrame;
struct sc_delayed_frame {
AVFrame *frame;
#ifndef NDEBUG
#ifdef SC_BUFFERING_DEBUG
sc_tick push_date;
#endif
};

View File

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

View File

@ -43,6 +43,10 @@ sc_display_init(struct sc_display *display, SDL_Window *window,
display->mipmaps = false;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
display->gl_context = NULL;
#endif
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {

66
app/src/events.c Normal file
View File

@ -0,0 +1,66 @@
#include "events.h"
#include "util/log.h"
#include "util/thread.h"
bool
sc_push_event_impl(uint32_t type, const char *name) {
SDL_Event event;
event.type = type;
int ret = SDL_PushEvent(&event);
// ret < 0: error (queue full)
// ret == 0: event was filtered
// ret == 1: success
if (ret != 1) {
LOGE("Could not post %s event: %s", name, SDL_GetError());
return false;
}
return true;
}
bool
sc_post_to_main_thread(sc_runnable_fn run, void *userdata) {
SDL_Event event = {
.user = {
.type = SC_EVENT_RUN_ON_MAIN_THREAD,
.data1 = run,
.data2 = userdata,
},
};
int ret = SDL_PushEvent(&event);
// ret < 0: error (queue full)
// ret == 0: event was filtered
// ret == 1: success
if (ret != 1) {
if (ret == 0) {
// if ret == 0, this is expected on exit, log in debug mode
LOGD("Could not post runnable to main thread (filtered)");
} else {
assert(ret < 0);
LOGW("Could not post runnable to main thread: %s", SDL_GetError());
}
return false;
}
return true;
}
static int SDLCALL
task_event_filter(void *userdata, SDL_Event *event) {
(void) userdata;
if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Reject this event type from now on
return 0;
}
return 1;
}
void
sc_reject_new_runnables(void) {
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
SDL_SetEventFilter(task_event_filter, NULL);
}

View File

@ -1,10 +1,38 @@
#define SC_EVENT_NEW_FRAME SDL_USEREVENT
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1)
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)
#define SC_EVENT_CONTROLLER_ERROR (SDL_USEREVENT + 9)
#ifndef SC_EVENTS_H
#define SC_EVENTS_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <SDL_events.h>
enum {
SC_EVENT_NEW_FRAME = SDL_USEREVENT,
SC_EVENT_RUN_ON_MAIN_THREAD,
SC_EVENT_DEVICE_DISCONNECTED,
SC_EVENT_SERVER_CONNECTION_FAILED,
SC_EVENT_SERVER_CONNECTED,
SC_EVENT_USB_DEVICE_DISCONNECTED,
SC_EVENT_DEMUXER_ERROR,
SC_EVENT_RECORDER_ERROR,
SC_EVENT_SCREEN_INIT_SIZE,
SC_EVENT_TIME_LIMIT_REACHED,
SC_EVENT_CONTROLLER_ERROR,
SC_EVENT_AOA_OPEN_ERROR,
};
bool
sc_push_event_impl(uint32_t type, const char *name);
#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE)
typedef void (*sc_runnable_fn)(void *userdata);
bool
sc_post_to_main_thread(sc_runnable_fn run, void *userdata);
void
sc_reject_new_runnables(void);
#endif

View File

@ -5,11 +5,23 @@
#include <stdint.h>
#define SC_HID_MAX_SIZE 8
#define SC_HID_MAX_SIZE 15
struct sc_hid_event {
struct sc_hid_input {
uint16_t hid_id;
uint8_t data[SC_HID_MAX_SIZE];
uint8_t size;
};
struct sc_hid_open {
uint16_t hid_id;
const char *name; // pointer to static memory
const uint8_t *report_desc; // pointer to static memory
size_t report_desc_size;
};
struct sc_hid_close {
uint16_t hid_id;
};
#endif

457
app/src/hid/hid_gamepad.c Normal file
View File

@ -0,0 +1,457 @@
#include "hid_gamepad.h"
#include <assert.h>
#include <inttypes.h>
#include "util/binary.h"
#include "util/log.h"
// 2x2 bytes for left stick (X, Y)
// 2x2 bytes for right stick (Z, Rz)
// 2x2 bytes for L2/R2 triggers
// 2 bytes for buttons + padding,
// 1 byte for hat switch (dpad) + padding
#define SC_HID_GAMEPAD_EVENT_SIZE 15
// The ->buttons field stores the state for all buttons, but only some of them
// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are
// stored locally in the MSB of this field, but not transmitted as is: they are
// transformed to generate another specific byte.
#define SC_HID_BUTTONS_MASK 0xFFFF
// outside SC_HID_BUTTONS_MASK
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000)
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000)
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000)
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000)
/**
* Gamepad descriptor manually crafted to transmit the input reports.
*
* The HID specification is available here:
* <https://www.usb.org/document-library/device-class-definition-hid-111>
*
* The HID Usage Tables is also useful:
* <https://www.usb.org/document-library/hid-usage-tables-15>
*/
static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Gamepad)
0x09, 0x05,
// Collection (Application)
0xA1, 0x01,
// Collection (Physical)
0xA1, 0x00,
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (X) Left stick x
0x09, 0x30,
// Usage (Y) Left stick y
0x09, 0x31,
// Usage (Z) Right stick x
0x09, 0x32,
// Usage (Rz) Right stick y
0x09, 0x35,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (65535)
// Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit
0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian
// Report Size (16)
0x75, 0x10,
// Report Count (4)
0x95, 0x04,
// Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz)
0x81, 0x02,
// Usage Page (Simulation Controls)
0x05, 0x02,
// Usage (Brake)
0x09, 0xC5,
// Usage (Accelerator)
0x09, 0xC4,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (32767)
0x26, 0xFF, 0x7F,
// Report Size (16)
0x75, 0x10,
// Report Count (2)
0x95, 0x02,
// Input (Data, Variable, Absolute): 2 bytes (L2, R2)
0x81, 0x02,
// Usage Page (Buttons)
0x05, 0x09,
// Usage Minimum (1)
0x19, 0x01,
// Usage Maximum (16)
0x29, 0x10,
// Logical Minimum (0)
0x15, 0x00,
// Logical Maximum (1)
0x25, 0x01,
// Report Count (16)
0x95, 0x10,
// Report Size (1)
0x75, 0x01,
// Input (Data, Variable, Absolute): 16 buttons bits
0x81, 0x02,
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Hat switch)
0x09, 0x39,
// Logical Minimum (1)
0x15, 0x01,
// Logical Maximum (8)
0x25, 0x08,
// Report Size (4)
0x75, 0x04,
// Report Count (1)
0x95, 0x01,
// Input (Data, Variable, Null State): 4-bit value
0x81, 0x42,
// End Collection
0xC0,
// End Collection
0xC0,
};
/**
* A gamepad HID input report is 15 bytes long:
* - bytes 0-3: left stick state
* - bytes 4-7: right stick state
* - bytes 8-11: L2/R2 triggers state
* - bytes 12-13: buttons state
* - bytes 14: hat switch position (dpad)
*
* +---------------+
* byte 0: |. . . . . . . .|
* | | left stick x (0-65535, little-endian)
* byte 1: |. . . . . . . .|
* +---------------+
* byte 2: |. . . . . . . .|
* | | left stick y (0-65535, little-endian)
* byte 3: |. . . . . . . .|
* +---------------+
* byte 4: |. . . . . . . .|
* | | right stick x (0-65535, little-endian)
* byte 5: |. . . . . . . .|
* +---------------+
* byte 6: |. . . . . . . .|
* | | right stick y (0-65535, little-endian)
* byte 7: |. . . . . . . .|
* +---------------+
* byte 8: |. . . . . . . .|
* | | L2 trigger (0-32767, little-endian)
* byte 9: |0 . . . . . . .|
* +---------------+
* byte 10: |. . . . . . . .|
* | | R2 trigger (0-32767, little-endian)
* byte 11: |0 . . . . . . .|
* +---------------+
*
* ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER
* | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER
* | |
* | | ,--------- SC_GAMEPAD_BUTTON_NORTH
* | | | ,------- SC_GAMEPAD_BUTTON_WEST
* | | | |
* | | | | ,--- SC_GAMEPAD_BUTTON_EAST
* | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH
* v v v v v v
* +---------------+
* byte 12: |. . 0 . . 0 . .|
* | | Buttons (16-bit little-endian)
* byte 13: |0 . . . . . 0 0|
* +---------------+
* ^ ^ ^ ^ ^
* | | | | |
* | | | | |
* | | | | `----- SC_GAMEPAD_BUTTON_BACK
* | | | `------- SC_GAMEPAD_BUTTON_START
* | | `--------- SC_GAMEPAD_BUTTON_GUIDE
* | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK
* `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK
*
* +---------------+
* byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8)
* +---------------+
* 9 possible positions and their values:
* 8 1 2
* 7 0 3
* 6 5 4
* (8 is top-left, 1 is top, 2 is top-right, etc.)
*/
static void
sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot,
uint32_t gamepad_id) {
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
slot->gamepad_id = gamepad_id;
slot->buttons = 0;
slot->axis_left_x = 0;
slot->axis_left_y = 0;
slot->axis_right_x = 0;
slot->axis_right_y = 0;
slot->axis_left_trigger = 0;
slot->axis_right_trigger = 0;
}
static ssize_t
sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) {
for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) {
if (gamepad_id == hid->slots[i].gamepad_id) {
// found
return i;
}
}
return -1;
}
void
sc_hid_gamepad_init(struct sc_hid_gamepad *hid) {
for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) {
hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID;
}
}
static inline uint16_t
sc_hid_gamepad_slot_get_id(size_t slot_idx) {
assert(slot_idx < SC_MAX_GAMEPADS);
return SC_HID_ID_GAMEPAD_FIRST + slot_idx;
}
bool
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
struct sc_hid_open *hid_open,
uint32_t gamepad_id) {
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID);
if (slot_idx == -1) {
LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id);
return false;
}
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id);
SDL_GameController* game_controller =
SDL_GameControllerFromInstanceID(gamepad_id);
assert(game_controller);
const char *name = SDL_GameControllerName(game_controller);
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
hid_open->hid_id = hid_id;
hid_open->name = name;
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
return true;
}
bool
sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid,
struct sc_hid_close *hid_close,
uint32_t gamepad_id) {
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
if (slot_idx == -1) {
LOGW("Unknown gamepad removed %" PRIu32, gamepad_id);
return false;
}
hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID;
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
hid_close->hid_id = hid_id;
return true;
}
static uint8_t
sc_hid_gamepad_get_dpad_value(uint32_t buttons) {
// Value depending on direction:
// 8 1 2
// 7 0 3
// 6 5 4
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) {
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
return 8;
}
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
return 2;
}
return 1;
}
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) {
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
return 6;
}
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
return 4;
}
return 5;
}
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
return 7;
}
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
return 3;
}
return 0;
}
static void
sc_hid_gamepad_event_from_slot(uint16_t hid_id,
const struct sc_hid_gamepad_slot *slot,
struct sc_hid_input *hid_input) {
hid_input->hid_id = hid_id;
hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE;
uint8_t *data = hid_input->data;
// Values must be written in little-endian
sc_write16le(data, slot->axis_left_x);
sc_write16le(data + 2, slot->axis_left_y);
sc_write16le(data + 4, slot->axis_right_x);
sc_write16le(data + 6, slot->axis_right_y);
sc_write16le(data + 8, slot->axis_left_trigger);
sc_write16le(data + 10, slot->axis_right_trigger);
sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK);
data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons);
}
static uint32_t
sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) {
switch (button) {
case SC_GAMEPAD_BUTTON_SOUTH:
return 0x0001;
case SC_GAMEPAD_BUTTON_EAST:
return 0x0002;
case SC_GAMEPAD_BUTTON_WEST:
return 0x0008;
case SC_GAMEPAD_BUTTON_NORTH:
return 0x0010;
case SC_GAMEPAD_BUTTON_BACK:
return 0x0400;
case SC_GAMEPAD_BUTTON_GUIDE:
return 0x1000;
case SC_GAMEPAD_BUTTON_START:
return 0x0800;
case SC_GAMEPAD_BUTTON_LEFT_STICK:
return 0x2000;
case SC_GAMEPAD_BUTTON_RIGHT_STICK:
return 0x4000;
case SC_GAMEPAD_BUTTON_LEFT_SHOULDER:
return 0x0040;
case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER:
return 0x0080;
case SC_GAMEPAD_BUTTON_DPAD_UP:
return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP;
case SC_GAMEPAD_BUTTON_DPAD_DOWN:
return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN;
case SC_GAMEPAD_BUTTON_DPAD_LEFT:
return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT;
case SC_GAMEPAD_BUTTON_DPAD_RIGHT:
return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT;
default:
// unknown button, ignore
return 0;
}
}
bool
sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid,
struct sc_hid_input *hid_input,
const struct sc_gamepad_button_event *event) {
if ((event->button < 0) || (event->button > 15)) {
return false;
}
uint32_t gamepad_id = event->gamepad_id;
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
if (slot_idx == -1) {
LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id);
return false;
}
assert(slot_idx < SC_MAX_GAMEPADS);
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
uint32_t button = sc_hid_gamepad_get_button_id(event->button);
if (!button) {
// unknown button, ignore
return false;
}
if (event->action == SC_ACTION_DOWN) {
slot->buttons |= button;
} else {
assert(event->action == SC_ACTION_UP);
slot->buttons &= ~button;
}
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
return true;
}
bool
sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
struct sc_hid_input *hid_input,
const struct sc_gamepad_axis_event *event) {
uint32_t gamepad_id = event->gamepad_id;
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
if (slot_idx == -1) {
LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id);
return false;
}
assert(slot_idx < SC_MAX_GAMEPADS);
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
// [-32768 to 32767] -> [0 to 65535]
#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000)
switch (event->axis) {
case SC_GAMEPAD_AXIS_LEFTX:
slot->axis_left_x = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_LEFTY:
slot->axis_left_y = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_RIGHTX:
slot->axis_right_x = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_RIGHTY:
slot->axis_right_y = AXIS_RESCALE(event->value);
break;
case SC_GAMEPAD_AXIS_LEFT_TRIGGER:
// Trigger is always positive between 0 and 32767
slot->axis_left_trigger = MAX(0, event->value);
break;
case SC_GAMEPAD_AXIS_RIGHT_TRIGGER:
// Trigger is always positive between 0 and 32767
slot->axis_right_trigger = MAX(0, event->value);
break;
default:
return false;
}
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
return true;
}

53
app/src/hid/hid_gamepad.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef SC_HID_GAMEPAD_H
#define SC_HID_GAMEPAD_H
#include "common.h"
#include <stdbool.h>
#include "hid/hid_event.h"
#include "input_events.h"
#define SC_MAX_GAMEPADS 8
#define SC_HID_ID_GAMEPAD_FIRST 3
#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1)
struct sc_hid_gamepad_slot {
uint32_t gamepad_id;
uint32_t buttons;
uint16_t axis_left_x;
uint16_t axis_left_y;
uint16_t axis_right_x;
uint16_t axis_right_y;
uint16_t axis_left_trigger;
uint16_t axis_right_trigger;
};
struct sc_hid_gamepad {
struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS];
};
void
sc_hid_gamepad_init(struct sc_hid_gamepad *hid);
bool
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
struct sc_hid_open *hid_open,
uint32_t gamepad_id);
bool
sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid,
struct sc_hid_close *hid_close,
uint32_t gamepad_id);
bool
sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid,
struct sc_hid_input *hid_input,
const struct sc_gamepad_button_event *event);
bool
sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
struct sc_hid_input *hid_input,
const struct sc_gamepad_axis_event *event);
#endif

View File

@ -21,7 +21,7 @@
// keyboard support, though OS could support more keys via modifying the report
// desc. 6 should be enough for scrcpy.
#define SC_HID_KEYBOARD_MAX_KEYS 6
#define SC_HID_KEYBOARD_EVENT_SIZE \
#define SC_HID_KEYBOARD_INPUT_SIZE \
(SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS)
#define SC_HID_RESERVED 0x00
@ -31,13 +31,16 @@
* For HID, only report descriptor is needed.
*
* The specification is available here:
* <https://www.usb.org/sites/default/files/hid1_11.pdf>
* <https://www.usb.org/document-library/device-class-definition-hid-111>
*
* In particular, read:
* - 6.2.2 Report Descriptor
* - §6.2.2 Report Descriptor
* - Appendix B.1 Protocol 1 (Keyboard)
* - Appendix C: Keyboard Implementation
*
* The HID Usage Tables is also useful:
* <https://www.usb.org/document-library/hid-usage-tables-15>
*
* Normally a basic HID keyboard uses 8 bytes:
* Modifier Reserved Key Key Key Key Key Key
*
@ -47,7 +50,7 @@
*
* (change vid:pid' to your device's vendor ID and product ID).
*/
const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Keyboard)
@ -60,7 +63,7 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
0x05, 0x07,
// Usage Minimum (224)
0x19, 0xE0,
// Usage Maximum (231)
// Usage Maximum (231)
0x29, 0xE7,
// Logical Minimum (0)
0x15, 0x00,
@ -121,11 +124,8 @@ const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
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 input report is 8 bytes long:
*
* - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys)
* - byte 1: reserved (always 0)
@ -199,10 +199,11 @@ const size_t SC_HID_KEYBOARD_REPORT_DESC_LEN =
*/
static void
sc_hid_keyboard_event_init(struct sc_hid_event *hid_event) {
hid_event->size = SC_HID_KEYBOARD_EVENT_SIZE;
sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) {
hid_input->hid_id = SC_HID_ID_KEYBOARD;
hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE;
uint8_t *data = hid_event->data;
uint8_t *data = hid_input->data;
data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE;
data[1] = SC_HID_RESERVED;
@ -250,9 +251,9 @@ scancode_is_modifier(enum sc_scancode scancode) {
}
bool
sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid,
struct sc_hid_event *hid_event,
const struct sc_key_event *event) {
sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid,
struct sc_hid_input *hid_input,
const struct sc_key_event *event) {
enum sc_scancode scancode = event->scancode;
assert(scancode >= 0);
@ -264,7 +265,7 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid,
return false;
}
sc_hid_keyboard_event_init(hid_event);
sc_hid_keyboard_input_init(hid_input);
uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state);
@ -275,9 +276,9 @@ sc_hid_keyboard_event_from_key(struct sc_hid_keyboard *hid,
hid->keys[scancode] ? "true" : "false");
}
hid_event->data[SC_HID_KEYBOARD_INDEX_MODS] = mods;
hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods;
uint8_t *keys_data = &hid_event->data[SC_HID_KEYBOARD_INDEX_KEYS];
uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS];
// Re-calculate pressed keys every time
int keys_pressed_count = 0;
for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) {
@ -308,8 +309,8 @@ end:
}
bool
sc_hid_keyboard_event_from_mods(struct sc_hid_event *event,
uint16_t mods_state) {
sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
uint16_t mods_state) {
bool capslock = mods_state & SC_MOD_CAPS;
bool numlock = mods_state & SC_MOD_NUM;
if (!capslock && !numlock) {
@ -317,17 +318,28 @@ sc_hid_keyboard_event_from_mods(struct sc_hid_event *event,
return false;
}
sc_hid_keyboard_event_init(event);
sc_hid_keyboard_input_init(hid_input);
unsigned i = 0;
if (capslock) {
event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK;
hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK;
++i;
}
if (numlock) {
event->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK;
hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK;
++i;
}
return true;
}
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_KEYBOARD;
hid_open->name = NULL; // No name specified after "scrcpy"
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
}
void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) {
hid_close->hid_id = SC_HID_ID_KEYBOARD;
}

View File

@ -14,8 +14,7 @@
// 0x65 is Application, typically AT-101 Keyboard ends here.
#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;
#define SC_HID_ID_KEYBOARD 1
/**
* HID keyboard events are sequence-based, every time keyboard state changes
@ -36,13 +35,19 @@ struct sc_hid_keyboard {
void
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);
void
sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open);
void
sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close);
bool
sc_hid_keyboard_event_from_mods(struct sc_hid_event *event,
uint16_t mods_state);
sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid,
struct sc_hid_input *hid_input,
const struct sc_key_event *event);
bool
sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
uint16_t mods_state);
#endif

View File

@ -2,19 +2,19 @@
// 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
#define SC_HID_MOUSE_INPUT_SIZE 4
/**
* Mouse descriptor from the specification:
* <https://www.usb.org/sites/default/files/hid1_11.pdf>
* <https://www.usb.org/document-library/device-class-definition-hid-111>
*
* 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)
* <https://www.usb.org/document-library/hid-usage-tables-15>
* §4 Generic Desktop Page (0x01) (p32)
*/
const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Usage Page (Generic Desktop)
0x05, 0x01,
// Usage (Mouse)
@ -34,7 +34,7 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
// Usage Minimum (1)
0x19, 0x01,
// Usage Maximum (5)
// Usage Maximum (5)
0x29, 0x05,
// Logical Minimum (0)
0x15, 0x00,
@ -62,9 +62,9 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
0x09, 0x31,
// Usage (Wheel)
0x09, 0x38,
// Local Minimum (-127)
// Logical Minimum (-127)
0x15, 0x81,
// Local Maximum (127)
// Logical Maximum (127)
0x25, 0x7F,
// Report Size (8)
0x75, 0x08,
@ -80,11 +80,8 @@ const uint8_t SC_HID_MOUSE_REPORT_DESC[] = {
0xC0,
};
const size_t SC_HID_MOUSE_REPORT_DESC_LEN =
sizeof(SC_HID_MOUSE_REPORT_DESC);
/**
* A mouse HID event is 4 bytes long:
* A mouse HID input report is 4 bytes long:
*
* - byte 0: buttons state
* - byte 1: relative x motion (signed byte from -127 to 127)
@ -125,10 +122,10 @@ const size_t SC_HID_MOUSE_REPORT_DESC_LEN =
*/
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
sc_hid_mouse_input_init(struct sc_hid_input *hid_input) {
hid_input->hid_id = SC_HID_ID_MOUSE;
hid_input->size = SC_HID_MOUSE_INPUT_SIZE;
// Leave ->data uninitialized, it will be fully initialized by callers
}
static uint8_t
@ -153,11 +150,11 @@ sc_hid_buttons_from_buttons_state(uint8_t buttons_state) {
}
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);
sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
const struct sc_mouse_motion_event *event) {
sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_event->data;
uint8_t *data = hid_input->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);
@ -165,11 +162,11 @@ sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event,
}
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);
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event) {
sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_event->data;
uint8_t *data = hid_input->data;
data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state);
data[1] = 0; // no x motion
data[2] = 0; // no y motion
@ -177,11 +174,11 @@ sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event,
}
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);
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event) {
sc_hid_mouse_input_init(hid_input);
uint8_t *data = hid_event->data;
uint8_t *data = hid_input->data;
data[0] = 0; // buttons state irrelevant (and unknown)
data[1] = 0; // no x motion
data[2] = 0; // no y motion
@ -190,3 +187,14 @@ sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event,
data[3] = CLAMP(event->vscroll, -127, 127);
// Horizontal scrolling ignored
}
void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) {
hid_open->hid_id = SC_HID_ID_MOUSE;
hid_open->name = NULL; // No name specified after "scrcpy"
hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC;
hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC);
}
void sc_hid_mouse_generate_close(struct sc_hid_close *hid_close) {
hid_close->hid_id = SC_HID_ID_MOUSE;
}

View File

@ -1,8 +1,6 @@
#ifndef SC_HID_MOUSE_H
#define SC_HID_MOUSE_H
#endif
#include "common.h"
#include <stdbool.h>
@ -10,17 +8,24 @@
#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;
#define SC_HID_ID_MOUSE 2
void
sc_hid_mouse_event_from_motion(struct sc_hid_event *hid_event,
const struct sc_mouse_motion_event *event);
sc_hid_mouse_generate_open(struct sc_hid_open *hid_open);
void
sc_hid_mouse_event_from_click(struct sc_hid_event *hid_event,
const struct sc_mouse_click_event *event);
sc_hid_mouse_generate_close(struct sc_hid_close *hid_close);
void
sc_hid_mouse_event_from_scroll(struct sc_hid_event *hid_event,
const struct sc_mouse_scroll_event *event);
sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input,
const struct sc_mouse_motion_event *event);
void
sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input,
const struct sc_mouse_click_event *event);
void
sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input,
const struct sc_mouse_scroll_event *event);
#endif

View File

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

View File

@ -9,6 +9,7 @@
#include <SDL2/SDL_events.h>
#include "coords.h"
#include "options.h"
/* The representation of input events in scrcpy is very close to the SDL API,
* for simplicity.
@ -322,6 +323,38 @@ enum sc_mouse_button {
SC_MOUSE_BUTTON_X2 = SDL_BUTTON(SDL_BUTTON_X2),
};
// Use the naming from SDL3 for gamepad axis and buttons:
// <https://wiki.libsdl.org/SDL3/README/migration>
enum sc_gamepad_axis {
SC_GAMEPAD_AXIS_UNKNOWN = -1,
SC_GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX,
SC_GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY,
SC_GAMEPAD_AXIS_RIGHTX = SDL_CONTROLLER_AXIS_RIGHTX,
SC_GAMEPAD_AXIS_RIGHTY = SDL_CONTROLLER_AXIS_RIGHTY,
SC_GAMEPAD_AXIS_LEFT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERLEFT,
SC_GAMEPAD_AXIS_RIGHT_TRIGGER = SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
};
enum sc_gamepad_button {
SC_GAMEPAD_BUTTON_UNKNOWN = -1,
SC_GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A,
SC_GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B,
SC_GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X,
SC_GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y,
SC_GAMEPAD_BUTTON_BACK = SDL_CONTROLLER_BUTTON_BACK,
SC_GAMEPAD_BUTTON_GUIDE = SDL_CONTROLLER_BUTTON_GUIDE,
SC_GAMEPAD_BUTTON_START = SDL_CONTROLLER_BUTTON_START,
SC_GAMEPAD_BUTTON_LEFT_STICK = SDL_CONTROLLER_BUTTON_LEFTSTICK,
SC_GAMEPAD_BUTTON_RIGHT_STICK = SDL_CONTROLLER_BUTTON_RIGHTSTICK,
SC_GAMEPAD_BUTTON_LEFT_SHOULDER = SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
SC_GAMEPAD_BUTTON_RIGHT_SHOULDER = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
SC_GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP,
SC_GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN,
SC_GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT,
SC_GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
};
static_assert(sizeof(enum sc_mod) >= sizeof(SDL_Keymod),
"SDL_Keymod must be convertible to sc_mod");
@ -379,6 +412,33 @@ struct sc_touch_event {
float pressure;
};
enum sc_gamepad_device_event_type {
SC_GAMEPAD_DEVICE_ADDED,
SC_GAMEPAD_DEVICE_REMOVED,
};
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
// The ID value starts at 0 and increments from there. The value -1 is an
// invalid ID.
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
struct sc_gamepad_device_event {
enum sc_gamepad_device_event_type type;
uint32_t gamepad_id;
};
struct sc_gamepad_button_event {
uint32_t gamepad_id;
enum sc_action action;
enum sc_gamepad_button button;
};
struct sc_gamepad_axis_event {
uint32_t gamepad_id;
enum sc_gamepad_axis axis;
int16_t value;
};
static inline uint16_t
sc_mods_state_from_sdl(uint16_t mods_state) {
return mods_state;
@ -436,19 +496,50 @@ sc_mouse_button_from_sdl(uint8_t button) {
}
static inline uint8_t
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state,
bool forward_all_clicks) {
sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) {
assert(buttons_state < 0x100); // fits in uint8_t
uint8_t mask = SC_MOUSE_BUTTON_LEFT;
if (forward_all_clicks) {
mask |= SC_MOUSE_BUTTON_RIGHT
| SC_MOUSE_BUTTON_MIDDLE
| SC_MOUSE_BUTTON_X1
| SC_MOUSE_BUTTON_X2;
}
// SC_MOUSE_BUTTON_* constants are initialized from SDL_BUTTON(index)
return buttons_state;
}
return buttons_state & mask;
static inline enum sc_gamepad_device_event_type
sc_gamepad_device_event_type_from_sdl_type(uint32_t type) {
assert(type == SDL_CONTROLLERDEVICEADDED
|| type == SDL_CONTROLLERDEVICEREMOVED);
if (type == SDL_CONTROLLERDEVICEADDED) {
return SC_GAMEPAD_DEVICE_ADDED;
}
return SC_GAMEPAD_DEVICE_REMOVED;
}
static inline enum sc_gamepad_axis
sc_gamepad_axis_from_sdl(uint8_t axis) {
if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
// SC_GAMEPAD_AXIS_* constants are initialized from
// SDL_CONTROLLER_AXIS_*
return axis;
}
return SC_GAMEPAD_AXIS_UNKNOWN;
}
static inline enum sc_gamepad_button
sc_gamepad_button_from_sdl(uint8_t button) {
if (button <= SDL_CONTROLLER_BUTTON_DPAD_RIGHT) {
// SC_GAMEPAD_BUTTON_* constants are initialized from
// SDL_CONTROLLER_BUTTON_*
return button;
}
return SC_GAMEPAD_BUTTON_UNKNOWN;
}
static inline enum sc_action
sc_action_from_sdl_controllerbutton_type(uint32_t type) {
assert(type == SDL_CONTROLLERBUTTONDOWN || type == SDL_CONTROLLERBUTTONUP);
if (type == SDL_CONTROLLERBUTTONDOWN) {
return SC_ACTION_DOWN;
}
return SC_ACTION_UP;
}
#endif

View File

@ -10,7 +10,7 @@
#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI)
static inline uint16_t
to_sdl_mod(unsigned shortcut_mod) {
to_sdl_mod(uint8_t shortcut_mod) {
uint16_t sdl_mod = 0;
if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) {
sdl_mod |= KMOD_LCTRL;
@ -38,50 +38,49 @@ is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) {
// keep only the relevant modifier keys
sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK;
assert(im->sdl_shortcut_mods.count);
assert(im->sdl_shortcut_mods.count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < im->sdl_shortcut_mods.count; ++i) {
if (im->sdl_shortcut_mods.data[i] == sdl_mod) {
return true;
}
}
// at least one shortcut mod pressed?
return sdl_mod & im->sdl_shortcut_mods;
}
return false;
static bool
is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) {
return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL)
|| (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL)
|| (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT)
|| (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT)
|| (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI)
|| (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI);
}
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
// A key/mouse processor may not be present if there is no controller
assert((!params->kp && !params->mp) || params->controller);
assert((!params->kp && !params->mp && !params->gp) || params->controller);
// A processor must have ops initialized
assert(!params->kp || params->kp->ops);
assert(!params->mp || params->mp->ops);
assert(!params->gp || params->gp->ops);
im->controller = params->controller;
im->fp = params->fp;
im->screen = params->screen;
im->kp = params->kp;
im->mp = params->mp;
im->gp = params->gp;
im->forward_all_clicks = params->forward_all_clicks;
im->mouse_bindings = params->mouse_bindings;
im->legacy_paste = params->legacy_paste;
im->clipboard_autosync = params->clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods = params->shortcut_mods;
assert(shortcut_mods->count);
assert(shortcut_mods->count < SC_MAX_SHORTCUT_MODS);
for (unsigned i = 0; i < shortcut_mods->count; ++i) {
uint16_t sdl_mod = to_sdl_mod(shortcut_mods->data[i]);
assert(sdl_mod);
im->sdl_shortcut_mods.data[i] = sdl_mod;
}
im->sdl_shortcut_mods.count = shortcut_mods->count;
im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods);
im->vfinger_down = false;
im->vfinger_invert_x = false;
im->vfinger_invert_y = false;
im->mouse_buttons_state = 0;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
@ -370,9 +369,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id =
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pointer_id = SC_POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0;
@ -405,29 +402,33 @@ sc_input_manager_process_key(struct sc_input_manager *im,
bool paused = im->screen->paused;
bool video = im->screen->video;
SDL_Keycode keycode = event->keysym.sym;
SDL_Keycode sdl_keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
bool smod = is_shortcut_mod(im, mod);
// Either the modifier includes a shortcut modifier, or the key
// press/release is a modifier key.
// The second condition is necessary to ignore the release of the modifier
// key (because in this case mod is 0).
bool is_shortcut = is_shortcut_mod(im, mod)
|| is_shortcut_key(im, sdl_keycode);
if (down && !repeat) {
if (keycode == im->last_keycode && mod == im->last_mod) {
if (sdl_keycode == im->last_keycode && mod == im->last_mod) {
++im->key_repeat;
} else {
im->key_repeat = 0;
im->last_keycode = keycode;
im->last_keycode = sdl_keycode;
im->last_mod = mod;
}
}
// The shortcut modifier is pressed
if (smod) {
if (is_shortcut) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
switch (keycode) {
switch (sdl_keycode) {
case SDLK_h:
if (im->kp && !shift && !repeat && !paused) {
action_home(im, action);
@ -586,7 +587,7 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
bool is_ctrl_v = ctrl && !shift && keycode == SDLK_v && down && !repeat;
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_v && down && !repeat;
if (im->clipboard_autosync && is_ctrl_v) {
if (im->legacy_paste) {
// inject the text as input events
@ -614,10 +615,20 @@ sc_input_manager_process_key(struct sc_input_manager *im,
}
}
enum sc_keycode keycode = sc_keycode_from_sdl(sdl_keycode);
if (keycode == SC_KEYCODE_UNKNOWN) {
return;
}
enum sc_scancode scancode = sc_scancode_from_sdl(event->keysym.scancode);
if (scancode == SC_SCANCODE_UNKNOWN) {
return;
}
struct sc_key_event evt = {
.action = sc_action_from_sdl_keyboard_type(event->type),
.keycode = sc_keycode_from_sdl(event->keysym.sym),
.scancode = sc_scancode_from_sdl(event->keysym.scancode),
.keycode = keycode,
.scancode = scancode,
.repeat = event->repeat,
.mods_state = sc_mods_state_from_sdl(event->keysym.mod),
};
@ -653,13 +664,11 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
struct sc_mouse_motion_event evt = {
.position = sc_input_manager_get_position(im, event->x, event->y),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.pointer_id = im->vfinger_down ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state =
sc_mouse_buttons_state_from_sdl(event->state,
im->forward_all_clicks),
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_motion);
@ -710,6 +719,25 @@ sc_input_manager_process_touch(struct sc_input_manager *im,
im->mp->ops->process_touch(im->mp, &evt);
}
static enum sc_mouse_binding
sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
uint8_t sdl_button) {
switch (sdl_button) {
case SDL_BUTTON_LEFT:
return SC_MOUSE_BINDING_CLICK;
case SDL_BUTTON_RIGHT:
return bindings->right_click;
case SDL_BUTTON_MIDDLE:
return bindings->middle_click;
case SDL_BUTTON_X1:
return bindings->click4;
case SDL_BUTTON_X2:
return bindings->click5;
default:
return SC_MOUSE_BINDING_DISABLED;
}
}
static void
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
const SDL_MouseButtonEvent *event) {
@ -721,66 +749,104 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
bool control = im->controller;
bool paused = im->screen->paused;
bool down = event->type == SDL_MOUSEBUTTONDOWN;
if (!im->forward_all_clicks) {
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
if (im->kp && event->button == SDL_BUTTON_X1) {
action_app_switch(im, action);
enum sc_mouse_button button = sc_mouse_button_from_sdl(event->button);
if (button == SC_MOUSE_BUTTON_UNKNOWN) {
return;
}
if (!down) {
// Mark the button as released
im->mouse_buttons_state &= ~button;
}
SDL_Keymod keymod = SDL_GetModState();
bool ctrl_pressed = keymod & KMOD_CTRL;
bool shift_pressed = keymod & KMOD_SHIFT;
if (control && !paused) {
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
struct sc_mouse_binding_set *bindings = !shift_pressed
? &im->mouse_bindings.pri
: &im->mouse_bindings.sec;
enum sc_mouse_binding binding =
sc_input_manager_get_binding(bindings, event->button);
assert(binding != SC_MOUSE_BINDING_AUTO);
switch (binding) {
case SC_MOUSE_BINDING_DISABLED:
// ignore click
return;
}
if (event->button == SDL_BUTTON_X2 && down) {
if (event->clicks < 2) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
case SC_MOUSE_BINDING_BACK:
if (im->kp) {
press_back_or_turn_screen_on(im, action);
}
return;
}
if (im->kp && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im, action);
case SC_MOUSE_BINDING_HOME:
if (im->kp) {
action_home(im, action);
}
return;
}
if (im->kp && event->button == SDL_BUTTON_MIDDLE) {
action_home(im, action);
case SC_MOUSE_BINDING_APP_SWITCH:
if (im->kp) {
action_app_switch(im, action);
}
return;
}
}
// double-click on black borders resize to fit the device screen
bool video = im->screen->video;
if (video && event->button == SDL_BUTTON_LEFT && event->clicks == 2) {
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
case SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL:
if (down) {
sc_screen_resize_to_fit(im->screen);
if (event->clicks < 2) {
expand_notification_panel(im);
} else {
expand_settings_panel(im);
}
}
return;
}
default:
assert(binding == SC_MOUSE_BINDING_CLICK);
break;
}
}
// double-click on black borders resizes to fit the device screen
bool video = im->screen->video;
bool mouse_relative_mode = im->mp && im->mp->relative_mode;
if (video && !mouse_relative_mode && event->button == SDL_BUTTON_LEFT
&& event->clicks == 2) {
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
if (down) {
sc_screen_resize_to_fit(im->screen);
}
return;
}
// otherwise, send the click event to the device
}
if (!im->mp || paused) {
return;
}
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
if (down) {
// Mark the button as pressed
im->mouse_buttons_state |= button;
}
bool change_vfinger = event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) ||
(!down && im->vfinger_down));
bool use_finger = im->vfinger_down || change_vfinger;
struct sc_mouse_click_event evt = {
.position = sc_input_manager_get_position(im, event->x, event->y),
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
im->forward_all_clicks),
.button = button,
.pointer_id = use_finger ? SC_POINTER_ID_GENERIC_FINGER
: SC_POINTER_ID_MOUSE,
.buttons_state = im->mouse_buttons_state,
};
assert(im->mp->ops->process_mouse_click);
@ -806,14 +872,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
// can be used instead of Ctrl. The "virtual finger" has a position
// inverted with respect to the vertical axis of symmetry in the middle of
// the screen.
const SDL_Keymod keymod = SDL_GetModState();
const bool ctrl_pressed = keymod & KMOD_CTRL;
const bool shift_pressed = keymod & KMOD_SHIFT;
if (event->button == SDL_BUTTON_LEFT &&
((down && !im->vfinger_down &&
((ctrl_pressed && !shift_pressed) ||
(!ctrl_pressed && shift_pressed))) ||
(!down && im->vfinger_down))) {
if (change_vfinger) {
struct sc_point mouse =
sc_screen_convert_window_to_frame_coords(im->screen, event->x,
event->y);
@ -846,6 +905,7 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
int mouse_x;
int mouse_y;
uint32_t buttons = SDL_GetMouseState(&mouse_x, &mouse_y);
(void) buttons; // Actual buttons are tracked manually to ignore shortcuts
struct sc_mouse_scroll_event evt = {
.position = sc_input_manager_get_position(im, mouse_x, mouse_y),
@ -856,13 +916,84 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.buttons_state =
sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks),
.buttons_state = im->mouse_buttons_state,
};
im->mp->ops->process_mouse_scroll(im->mp, &evt);
}
static void
sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
const SDL_ControllerDeviceEvent *event) {
SDL_JoystickID id;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) {
LOGW("Could not open game controller");
return;
}
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc);
if (!joystick) {
LOGW("Could not get controller joystick");
SDL_GameControllerClose(gc);
return;
}
id = SDL_JoystickInstanceID(joystick);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) {
SDL_GameControllerClose(gc);
} else {
LOGW("Unknown gamepad device removed");
}
} else {
// Nothing to do
return;
}
struct sc_gamepad_device_event evt = {
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
.gamepad_id = id,
};
im->gp->ops->process_gamepad_device(im->gp, &evt);
}
static void
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
const SDL_ControllerAxisEvent *event) {
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
}
struct sc_gamepad_axis_event evt = {
.gamepad_id = event->which,
.axis = axis,
.value = event->value,
};
im->gp->ops->process_gamepad_axis(im->gp, &evt);
}
static void
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
const SDL_ControllerButtonEvent *event) {
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
}
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.button = button,
};
im->gp->ops->process_gamepad_button(im->gp, &evt);
}
static bool
is_apk(const char *file) {
const char *ext = strrchr(file, '.');
@ -935,6 +1066,27 @@ sc_input_manager_handle_event(struct sc_input_manager *im,
}
sc_input_manager_process_touch(im, &event->tfinger);
break;
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
// Handle device added or removed even if paused
if (!im->gp) {
break;
}
sc_input_manager_process_gamepad_device(im, &event->cdevice);
break;
case SDL_CONTROLLERAXISMOTION:
if (!im->gp || paused) {
break;
}
sc_input_manager_process_gamepad_axis(im, &event->caxis);
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
if (!im->gp || paused) {
break;
}
sc_input_manager_process_gamepad_button(im, &event->cbutton);
break;
case SDL_DROPFILE: {
if (!control) {
break;

View File

@ -11,6 +11,7 @@
#include "file_pusher.h"
#include "fps_counter.h"
#include "options.h"
#include "trait/gamepad_processor.h"
#include "trait/key_processor.h"
#include "trait/mouse_processor.h"
@ -21,20 +22,20 @@ struct sc_input_manager {
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
struct sc_gamepad_processor *gp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
struct {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
} sdl_shortcut_mods;
uint16_t sdl_shortcut_mods;
bool vfinger_down;
bool vfinger_invert_x;
bool vfinger_invert_y;
uint8_t mouse_buttons_state; // OR of enum sc_mouse_button values
// Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of
// system-generated repeated key presses.
@ -51,11 +52,12 @@ struct sc_input_manager_params {
struct sc_screen *screen;
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
struct sc_gamepad_processor *gp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
void

View File

@ -16,6 +16,7 @@
#include "usb/scrcpy_otg.h"
#include "util/log.h"
#include "util/net.h"
#include "util/thread.h"
#include "version.h"
#ifdef _WIN32
@ -67,6 +68,9 @@ main_scrcpy(int argc, char *argv[]) {
goto end;
}
// The current thread is the main thread
SC_MAIN_THREAD_ID = sc_thread_get_id();
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif

View File

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

View File

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

View File

@ -23,6 +23,21 @@ const struct scrcpy_options scrcpy_options_default = {
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AUTO,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_AUTO,
.gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED,
.mouse_bindings = {
.pri = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
.sec = {
.right_click = SC_MOUSE_BINDING_AUTO,
.middle_click = SC_MOUSE_BINDING_AUTO,
.click4 = SC_MOUSE_BINDING_AUTO,
.click5 = SC_MOUSE_BINDING_AUTO,
},
},
.camera_facing = SC_CAMERA_FACING_ANY,
.port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
@ -30,14 +45,11 @@ const struct scrcpy_options scrcpy_options_default = {
},
.tunnel_host = 0,
.tunnel_port = 0,
.shortcut_mods = {
.data = {SC_SHORTCUT_MOD_LALT, SC_SHORTCUT_MOD_LSUPER},
.count = 2,
},
.shortcut_mods = SC_SHORTCUT_MOD_LALT | SC_SHORTCUT_MOD_LSUPER,
.max_size = 0,
.video_bit_rate = 0,
.audio_bit_rate = 0,
.max_fps = 0,
.max_fps = NULL,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0,
@ -71,7 +83,6 @@ const struct scrcpy_options scrcpy_options_default = {
.force_adb_forward = false,
.disable_screensaver = false,
.forward_key_repeat = true,
.forward_all_clicks = false,
.legacy_paste = false,
.power_off_on_close = false,
.clipboard_autosync = true,
@ -90,6 +101,8 @@ const struct scrcpy_options scrcpy_options_default = {
.camera_high_speed = false,
.list = 0,
.window = true,
.mouse_hover = true,
.audio_dup = false,
};
enum sc_orientation

View File

@ -59,6 +59,7 @@ enum sc_audio_source {
SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
SC_AUDIO_SOURCE_PLAYBACK,
};
enum sc_camera_facing {
@ -141,6 +142,7 @@ enum sc_lock_video_orientation {
enum sc_keyboard_input_mode {
SC_KEYBOARD_INPUT_MODE_AUTO,
SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_KEYBOARD_INPUT_MODE_DISABLED,
SC_KEYBOARD_INPUT_MODE_SDK,
SC_KEYBOARD_INPUT_MODE_UHID,
@ -149,12 +151,42 @@ enum sc_keyboard_input_mode {
enum sc_mouse_input_mode {
SC_MOUSE_INPUT_MODE_AUTO,
SC_MOUSE_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_MOUSE_INPUT_MODE_DISABLED,
SC_MOUSE_INPUT_MODE_SDK,
SC_MOUSE_INPUT_MODE_UHID,
SC_MOUSE_INPUT_MODE_AOA,
};
enum sc_gamepad_input_mode {
SC_GAMEPAD_INPUT_MODE_DISABLED,
SC_GAMEPAD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode
SC_GAMEPAD_INPUT_MODE_UHID,
SC_GAMEPAD_INPUT_MODE_AOA,
};
enum sc_mouse_binding {
SC_MOUSE_BINDING_AUTO,
SC_MOUSE_BINDING_DISABLED,
SC_MOUSE_BINDING_CLICK,
SC_MOUSE_BINDING_BACK,
SC_MOUSE_BINDING_HOME,
SC_MOUSE_BINDING_APP_SWITCH,
SC_MOUSE_BINDING_EXPAND_NOTIFICATION_PANEL,
};
struct sc_mouse_binding_set {
enum sc_mouse_binding right_click;
enum sc_mouse_binding middle_click;
enum sc_mouse_binding click4;
enum sc_mouse_binding click5;
};
struct sc_mouse_bindings {
struct sc_mouse_binding_set pri;
struct sc_mouse_binding_set sec; // When Shift is pressed
};
enum sc_key_inject_mode {
// Inject special keys, letters and space as key events.
// Inject numbers and punctuation as text events.
@ -169,8 +201,6 @@ enum sc_key_inject_mode {
SC_KEY_INJECT_MODE_RAW,
};
#define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod {
SC_SHORTCUT_MOD_LCTRL = 1 << 0,
SC_SHORTCUT_MOD_RCTRL = 1 << 1,
@ -180,11 +210,6 @@ enum sc_shortcut_mod {
SC_SHORTCUT_MOD_RSUPER = 1 << 5,
};
struct sc_shortcut_mods {
unsigned data[SC_MAX_SHORTCUT_MODS];
unsigned count;
};
struct sc_port_range {
uint16_t first;
uint16_t last;
@ -215,15 +240,17 @@ struct scrcpy_options {
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;
enum sc_gamepad_input_mode gamepad_input_mode;
struct sc_mouse_bindings mouse_bindings;
enum sc_camera_facing camera_facing;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
struct sc_shortcut_mods shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
uint16_t max_size;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint16_t max_fps;
const char *max_fps; // float to be parsed by the server
enum sc_lock_video_orientation lock_video_orientation;
enum sc_orientation display_orientation;
enum sc_orientation record_orientation;
@ -257,7 +284,6 @@ struct scrcpy_options {
bool force_adb_forward;
bool disable_screensaver;
bool forward_key_repeat;
bool forward_all_clicks;
bool legacy_paste;
bool power_off_on_close;
bool clipboard_autosync;
@ -280,6 +306,8 @@ struct scrcpy_options {
#define SC_OPTION_LIST_CAMERA_SIZES 0x8
uint8_t list;
bool window;
bool mouse_hover;
bool audio_dup;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -6,8 +6,17 @@
#include <SDL2/SDL_clipboard.h>
#include "device_msg.h"
#include "events.h"
#include "util/log.h"
#include "util/str.h"
#include "util/thread.h"
struct sc_uhid_output_task_data {
struct sc_uhid_devices *uhid_devices;
uint16_t id;
uint16_t size;
uint8_t *data;
};
bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
@ -21,7 +30,7 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
receiver->acksync = NULL;
receiver->uhid_devices = NULL;
assert(cbs && cbs->on_error);
assert(cbs && cbs->on_ended);
receiver->cbs = cbs;
receiver->cbs_userdata = cbs_userdata;
@ -33,20 +42,52 @@ sc_receiver_destroy(struct sc_receiver *receiver) {
sc_mutex_destroy(&receiver->mutex);
}
static void
task_set_clipboard(void *userdata) {
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
char *text = userdata;
char *current = SDL_GetClipboardText();
bool same = current && !strcmp(current, text);
SDL_free(current);
if (same) {
LOGD("Computer clipboard unchanged");
} else {
LOGI("Device clipboard copied");
SDL_SetClipboardText(text);
}
free(text);
}
static void
task_uhid_output(void *userdata) {
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
struct sc_uhid_output_task_data *data = userdata;
sc_uhid_devices_process_hid_output(data->uhid_devices, data->id, data->data,
data->size);
free(data->data);
free(data);
}
static void
process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
bool same = current && !strcmp(current, msg->clipboard.text);
SDL_free(current);
if (same) {
LOGD("Computer clipboard unchanged");
// Take ownership of the text (do not destroy the msg)
char *text = msg->clipboard.text;
bool ok = sc_post_to_main_thread(task_set_clipboard, text);
if (!ok) {
LOGW("Could not post clipboard to main thread");
free(text);
return;
}
LOGI("Device clipboard copied");
SDL_SetClipboardText(msg->clipboard.text);
break;
}
case DEVICE_MSG_TYPE_ACK_CLIPBOARD:
@ -64,6 +105,7 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
}
sc_acksync_ack(receiver->acksync, msg->ack_clipboard.sequence);
// No allocation to free in the msg
break;
case DEVICE_MSG_TYPE_UHID_OUTPUT:
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
@ -79,26 +121,35 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
}
}
// 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");
sc_device_msg_destroy(msg);
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);
struct sc_uhid_output_task_data *data = malloc(sizeof(*data));
if (!data) {
LOG_OOM();
return;
}
// It is guaranteed that these pointers will still be valid when
// the main thread will process them (the main thread will stop
// processing SC_EVENT_RUN_ON_MAIN_THREAD on exit, when everything
// gets deinitialized)
data->uhid_devices = receiver->uhid_devices;
data->id = msg->uhid_output.id;
data->data = msg->uhid_output.data; // take ownership
data->size = msg->uhid_output.size;
bool ok = sc_post_to_main_thread(task_uhid_output, data);
if (!ok) {
LOGW("Could not post UHID output to main thread");
free(data->data);
free(data);
return;
}
break;
}
}
@ -117,7 +168,7 @@ process_msgs(struct sc_receiver *receiver, const uint8_t *buf, size_t len) {
}
process_msg(receiver, &msg);
sc_device_msg_destroy(&msg);
// the device msg must be destroyed by process_msg()
head += r;
assert(head <= len);
@ -134,12 +185,15 @@ run_receiver(void *data) {
static uint8_t buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0;
bool error = false;
for (;;) {
assert(head < DEVICE_MSG_MAX_SIZE);
ssize_t r = net_recv(receiver->control_socket, buf + head,
DEVICE_MSG_MAX_SIZE - head);
if (r <= 0) {
LOGD("Receiver stopped");
// device disconnected: keep error=false
break;
}
@ -147,6 +201,7 @@ run_receiver(void *data) {
ssize_t consumed = process_msgs(receiver, buf, head);
if (consumed == -1) {
// an error occurred
error = true;
break;
}
@ -157,7 +212,7 @@ run_receiver(void *data) {
}
}
receiver->cbs->on_error(receiver, receiver->cbs_userdata);
receiver->cbs->on_ended(receiver, error, receiver->cbs_userdata);
return 0;
}

View File

@ -25,7 +25,7 @@ struct sc_receiver {
};
struct sc_receiver_callbacks {
void (*on_error)(struct sc_receiver *receiver, void *userdata);
void (*on_ended)(struct sc_receiver *receiver, bool error, void *userdata);
};
bool

View File

@ -25,10 +25,12 @@
#include "recorder.h"
#include "screen.h"
#include "server.h"
#include "uhid/gamepad_uhid.h"
#include "uhid/keyboard_uhid.h"
#include "uhid/mouse_uhid.h"
#ifdef HAVE_USB
# include "usb/aoa_hid.h"
# include "usb/gamepad_aoa.h"
# include "usb/keyboard_aoa.h"
# include "usb/mouse_aoa.h"
# include "usb/usb.h"
@ -63,8 +65,8 @@ struct scrcpy {
struct sc_aoa aoa;
// sequence/ack helper to synchronize clipboard and Ctrl+v via HID
struct sc_acksync acksync;
struct sc_uhid_devices uhid_devices;
#endif
struct sc_uhid_devices uhid_devices;
union {
struct sc_keyboard_sdk keyboard_sdk;
struct sc_keyboard_uhid keyboard_uhid;
@ -77,27 +79,21 @@ struct scrcpy {
struct sc_mouse_uhid mouse_uhid;
#ifdef HAVE_USB
struct sc_mouse_aoa mouse_aoa;
#endif
};
union {
struct sc_gamepad_uhid gamepad_uhid;
#ifdef HAVE_USB
struct sc_gamepad_aoa gamepad_aoa;
#endif
};
struct sc_timeout timeout;
};
static inline void
push_event(uint32_t type, const char *name) {
SDL_Event event;
event.type = type;
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGE("Could not post %s event: %s", name, SDL_GetError());
// What could we do?
}
}
#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE)
#ifdef _WIN32
static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_C_EVENT) {
PUSH_EVENT(SDL_QUIT);
sc_push_event(SDL_QUIT);
return TRUE;
}
return FALSE;
@ -140,6 +136,10 @@ sdl_set_hints(const char *render_driver) {
if (!SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0")) {
LOGW("Could not disable minimize on focus loss");
}
if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) {
LOGW("Could not allow joystick background events");
}
}
static void
@ -180,12 +180,21 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_AOA_OPEN_ERROR:
LOGE("AOA open error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_TIME_LIMIT_REACHED:
LOGI("Time limit reached");
return SCRCPY_EXIT_SUCCESS;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
case SC_EVENT_RUN_ON_MAIN_THREAD: {
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
break;
}
default:
if (!sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
@ -196,6 +205,21 @@ event_loop(struct scrcpy *s) {
return SCRCPY_EXIT_FAILURE;
}
static void
terminate_event_loop(void) {
sc_reject_new_runnables();
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SC_EVENT_RUN_ON_MAIN_THREAD) {
// Make sure all posted runnables are run, to avoid memory leaks
sc_runnable_fn run = event.user.data1;
void *userdata = event.user.data2;
run(userdata);
}
}
}
// Return true on success, false on error
static bool
await_for_server(bool *connected) {
@ -230,7 +254,7 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
(void) userdata;
if (!success) {
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
sc_push_event(SC_EVENT_RECORDER_ERROR);
}
}
@ -244,9 +268,9 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
assert(status != SC_DEMUXER_STATUS_DISABLED);
if (status == SC_DEMUXER_STATUS_EOS) {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
sc_push_event(SC_EVENT_DEVICE_DISCONNECTED);
} else {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
sc_push_event(SC_EVENT_DEMUXER_ERROR);
}
}
@ -260,22 +284,27 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
// Contrary to the video demuxer, keep mirroring if only the audio fails
// (unless --require-audio is set).
if (status == SC_DEMUXER_STATUS_EOS) {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
sc_push_event(SC_EVENT_DEVICE_DISCONNECTED);
} else if (status == SC_DEMUXER_STATUS_ERROR
|| (status == SC_DEMUXER_STATUS_DISABLED
&& options->require_audio)) {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
sc_push_event(SC_EVENT_DEMUXER_ERROR);
}
}
static void
sc_controller_on_error(struct sc_controller *controller, void *userdata) {
sc_controller_on_ended(struct sc_controller *controller, bool error,
void *userdata) {
// Note: this function may be called twice, once from the controller thread
// and once from the receiver thread
(void) controller;
(void) userdata;
PUSH_EVENT(SC_EVENT_CONTROLLER_ERROR);
if (error) {
sc_push_event(SC_EVENT_CONTROLLER_ERROR);
} else {
sc_push_event(SC_EVENT_DEVICE_DISCONNECTED);
}
}
static void
@ -283,7 +312,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
sc_push_event(SC_EVENT_SERVER_CONNECTION_FAILED);
}
static void
@ -291,7 +320,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
sc_push_event(SC_EVENT_SERVER_CONNECTED);
}
static void
@ -309,7 +338,7 @@ sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) {
(void) timeout;
(void) userdata;
PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED);
sc_push_event(SC_EVENT_TIME_LIMIT_REACHED);
}
// Generate a scrcpy id to differentiate multiple running scrcpy instances
@ -321,6 +350,21 @@ scrcpy_generate_scid(void) {
return sc_rand_u32(&rand) & 0x7FFFFFFF;
}
static void
init_sdl_gamepads(void) {
// Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already
// connected
int num_joysticks = SDL_NumJoysticks();
for (int i = 0; i < num_joysticks; ++i) {
if (SDL_IsGameController(i)) {
SDL_Event event;
event.cdevice.type = SDL_CONTROLLERDEVICEADDED;
event.cdevice.which = i;
SDL_PushEvent(&event);
}
}
}
enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy;
@ -353,6 +397,7 @@ scrcpy(struct scrcpy_options *options) {
bool aoa_hid_initialized = false;
bool keyboard_aoa_initialized = false;
bool mouse_aoa_initialized = false;
bool gamepad_aoa_initialized = false;
#endif
bool controller_initialized = false;
bool controller_started = false;
@ -361,7 +406,6 @@ scrcpy(struct scrcpy_options *options) {
bool timeout_started = false;
struct sc_acksync *acksync = NULL;
struct sc_uhid_devices *uhid_devices = NULL;
uint32_t scid = scrcpy_generate_scid();
@ -389,6 +433,7 @@ scrcpy(struct scrcpy_options *options) {
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.audio_dup = options->audio_dup,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.video_codec_options = options->video_codec_options,
@ -467,6 +512,13 @@ scrcpy(struct scrcpy_options *options) {
}
}
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (SDL_Init(SDL_INIT_GAMECONTROLLER)) {
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
goto end;
}
}
sdl_configure(options->video_playback, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling
@ -564,10 +616,11 @@ scrcpy(struct scrcpy_options *options) {
struct sc_controller *controller = NULL;
struct sc_key_processor *kp = NULL;
struct sc_mouse_processor *mp = NULL;
struct sc_gamepad_processor *gp = NULL;
if (options->control) {
static const struct sc_controller_callbacks controller_cbs = {
.on_error = sc_controller_on_error,
.on_ended = sc_controller_on_ended,
};
if (!sc_controller_init(&s->controller, s->server.control_socket,
@ -583,7 +636,9 @@ scrcpy(struct scrcpy_options *options) {
options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA;
bool use_mouse_aoa =
options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA;
if (use_keyboard_aoa || use_mouse_aoa) {
bool use_gamepad_aoa =
options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA;
if (use_keyboard_aoa || use_mouse_aoa || use_gamepad_aoa) {
bool ok = sc_acksync_init(&s->acksync);
if (!ok) {
goto end;
@ -626,12 +681,15 @@ scrcpy(struct scrcpy_options *options) {
goto end;
}
bool aoa_fail = false;
if (use_keyboard_aoa) {
if (sc_keyboard_aoa_init(&s->keyboard_aoa, &s->aoa)) {
keyboard_aoa_initialized = true;
kp = &s->keyboard_aoa.key_processor;
} else {
LOGE("Could not initialize HID keyboard");
aoa_fail = true;
goto aoa_complete;
}
}
@ -641,12 +699,19 @@ scrcpy(struct scrcpy_options *options) {
mp = &s->mouse_aoa.mouse_processor;
} else {
LOGE("Could not initialized HID mouse");
aoa_fail = true;
goto aoa_complete;
}
}
bool need_aoa = keyboard_aoa_initialized || mouse_aoa_initialized;
if (use_gamepad_aoa) {
sc_gamepad_aoa_init(&s->gamepad_aoa, &s->aoa);
gp = &s->gamepad_aoa.gamepad_processor;
gamepad_aoa_initialized = true;
}
if (!need_aoa || !sc_aoa_start(&s->aoa)) {
aoa_complete:
if (aoa_fail || !sc_aoa_start(&s->aoa)) {
sc_acksync_destroy(&s->acksync);
sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb);
@ -663,6 +728,8 @@ scrcpy(struct scrcpy_options *options) {
assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_AOA);
#endif
struct sc_keyboard_uhid *uhid_keyboard = NULL;
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_SDK) {
sc_keyboard_sdk_init(&s->keyboard_sdk, &s->controller,
options->key_inject_mode,
@ -670,18 +737,17 @@ scrcpy(struct scrcpy_options *options) {
kp = &s->keyboard_sdk.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);
bool ok = sc_keyboard_uhid_init(&s->keyboard_uhid, &s->controller);
if (!ok) {
goto end;
}
uhid_devices = &s->uhid_devices;
kp = &s->keyboard_uhid.key_processor;
uhid_keyboard = &s->keyboard_uhid;
}
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_SDK) {
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller);
sc_mouse_sdk_init(&s->mouse_sdk, &s->controller,
options->mouse_hover);
mp = &s->mouse_sdk.mouse_processor;
} else if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_UHID) {
bool ok = sc_mouse_uhid_init(&s->mouse_uhid, &s->controller);
@ -691,6 +757,17 @@ scrcpy(struct scrcpy_options *options) {
mp = &s->mouse_uhid.mouse_processor;
}
if (options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_UHID) {
sc_gamepad_uhid_init(&s->gamepad_uhid, &s->controller);
gp = &s->gamepad_uhid.gamepad_processor;
}
struct sc_uhid_devices *uhid_devices = NULL;
if (uhid_keyboard) {
sc_uhid_devices_init(&s->uhid_devices, uhid_keyboard);
uhid_devices = &s->uhid_devices;
}
sc_controller_configure(&s->controller, acksync, uhid_devices);
if (!sc_controller_start(&s->controller)) {
@ -712,10 +789,11 @@ scrcpy(struct scrcpy_options *options) {
.fp = fp,
.kp = kp,
.mp = mp,
.forward_all_clicks = options->forward_all_clicks,
.gp = gp,
.mouse_bindings = options->mouse_bindings,
.legacy_paste = options->legacy_paste,
.clipboard_autosync = options->clipboard_autosync,
.shortcut_mods = &options->shortcut_mods,
.shortcut_mods = options->shortcut_mods,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@ -729,23 +807,20 @@ scrcpy(struct scrcpy_options *options) {
.start_fps_counter = options->start_fps_counter,
};
struct sc_frame_source *src;
if (options->video_playback) {
src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
}
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
if (options->video_playback) {
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer,
options->display_buffer, true);
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
src = &s->display_buffer.frame_source;
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
}
@ -826,7 +901,13 @@ scrcpy(struct scrcpy_options *options) {
timeout_started = true;
}
if (options->control
&& options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
init_sdl_gamepads();
}
ret = event_loop(s);
terminate_event_loop();
LOGD("quit...");
if (options->video_playback) {
@ -851,6 +932,9 @@ end:
if (mouse_aoa_initialized) {
sc_mouse_aoa_destroy(&s->mouse_aoa);
}
if (gamepad_aoa_initialized) {
sc_gamepad_aoa_destroy(&s->gamepad_aoa);
}
sc_aoa_stop(&s->aoa);
sc_usb_stop(&s->usb);
}

View File

@ -299,6 +299,12 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
struct sc_screen *screen = DOWNCAST(sink);
if (ctx->width <= 0 || ctx->width > 0xFFFF
|| ctx->height <= 0 || ctx->height > 0xFFFF) {
LOGE("Invalid video size: %dx%d", ctx->width, ctx->height);
return false;
}
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
// screen->frame_size is never used before the event is pushed, and the
@ -306,14 +312,9 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
screen->frame_size.width = ctx->width;
screen->frame_size.height = ctx->height;
static SDL_Event event = {
.type = SC_EVENT_SCREEN_INIT_SIZE,
};
// Post the event on the UI thread (the texture must be created from there)
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGW("Could not post init size event: %s", SDL_GetError());
bool ok = sc_push_event(SC_EVENT_SCREEN_INIT_SIZE);
if (!ok) {
return false;
}
@ -352,14 +353,9 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead
} else {
static SDL_Event new_frame_event = {
.type = SC_EVENT_NEW_FRAME,
};
// Post the event on the UI thread
int ret = SDL_PushEvent(&new_frame_event);
if (ret < 0) {
LOGW("Could not post new frame event: %s", SDL_GetError());
bool ok = sc_push_event(SC_EVENT_NEW_FRAME);
if (!ok) {
return false;
}
}
@ -481,7 +477,8 @@ sc_screen_init(struct sc_screen *screen,
.screen = screen,
.kp = params->kp,
.mp = params->mp,
.forward_all_clicks = params->forward_all_clicks,
.gp = params->gp,
.mouse_bindings = params->mouse_bindings,
.legacy_paste = params->legacy_paste,
.clipboard_autosync = params->clipboard_autosync,
.shortcut_mods = params->shortcut_mods,

View File

@ -78,11 +78,12 @@ struct sc_screen_params {
struct sc_file_pusher *fp;
struct sc_key_processor *kp;
struct sc_mouse_processor *mp;
struct sc_gamepad_processor *gp;
bool forward_all_clicks;
struct sc_mouse_bindings mouse_bindings;
bool legacy_paste;
bool clipboard_autosync;
const struct sc_shortcut_mods *shortcut_mods;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
const char *window_title;
bool always_on_top;

View File

@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) {
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
return NULL;
}
}
@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) {
case SC_CODEC_RAW:
return "raw";
default:
assert(!"unexpected codec");
return NULL;
}
}
@ -197,10 +198,41 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) {
case SC_CAMERA_FACING_EXTERNAL:
return "external";
default:
assert(!"unexpected camera facing");
return NULL;
}
}
static const char *
sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
switch (audio_source) {
case SC_AUDIO_SOURCE_OUTPUT:
return "output";
case SC_AUDIO_SOURCE_MIC:
return "mic";
case SC_AUDIO_SOURCE_PLAYBACK:
return "playback";
default:
assert(!"unexpected audio source");
return NULL;
}
}
static bool
validate_string(const char *s) {
// The parameters values are passed as command line arguments to adb, so
// they must either be properly escaped, or they must not contain any
// special shell characters.
// Since they are not properly escaped on Windows anyway (see
// sys/win/process.c), just forbid special shell characters.
if (strpbrk(s, " ;'\"*$?&`#\\|<>[]{}()!~\r\n")) {
LOGE("Invalid server param: [%s]", s);
return false;
}
return true;
}
static sc_pid
execute_server(struct sc_server *server,
const struct sc_server_params *params) {
@ -243,6 +275,11 @@ execute_server(struct sc_server *server,
} \
cmd[count++] = p; \
} while(0)
#define VALIDATE_STRING(s) do { \
if (!validate_string(s)) { \
goto end; \
} \
} while(0)
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
@ -271,14 +308,21 @@ execute_server(struct sc_server *server,
assert(params->video_source == SC_VIDEO_SOURCE_CAMERA);
ADD_PARAM("video_source=camera");
}
if (params->audio_source == SC_AUDIO_SOURCE_MIC) {
ADD_PARAM("audio_source=mic");
// If audio is enabled, an "auto" audio source must have been resolved
assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio);
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) {
ADD_PARAM("audio_source=%s",
sc_server_get_audio_source_name(params->audio_source));
}
if (params->audio_dup) {
ADD_PARAM("audio_dup=true");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}
if (params->max_fps) {
ADD_PARAM("max_fps=%" PRIu16, params->max_fps);
VALIDATE_STRING(params->max_fps);
ADD_PARAM("max_fps=%s", params->max_fps);
}
if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
ADD_PARAM("lock_video_orientation=%" PRIi8,
@ -288,6 +332,7 @@ execute_server(struct sc_server *server,
ADD_PARAM("tunnel_forward=true");
}
if (params->crop) {
VALIDATE_STRING(params->crop);
ADD_PARAM("crop=%s", params->crop);
}
if (!params->control) {
@ -298,9 +343,11 @@ execute_server(struct sc_server *server,
ADD_PARAM("display_id=%" PRIu32, params->display_id);
}
if (params->camera_id) {
VALIDATE_STRING(params->camera_id);
ADD_PARAM("camera_id=%s", params->camera_id);
}
if (params->camera_size) {
VALIDATE_STRING(params->camera_size);
ADD_PARAM("camera_size=%s", params->camera_size);
}
if (params->camera_facing != SC_CAMERA_FACING_ANY) {
@ -308,6 +355,7 @@ execute_server(struct sc_server *server,
sc_server_get_camera_facing_name(params->camera_facing));
}
if (params->camera_ar) {
VALIDATE_STRING(params->camera_ar);
ADD_PARAM("camera_ar=%s", params->camera_ar);
}
if (params->camera_fps) {
@ -323,15 +371,19 @@ execute_server(struct sc_server *server,
ADD_PARAM("stay_awake=true");
}
if (params->video_codec_options) {
VALIDATE_STRING(params->video_codec_options);
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
}
if (params->audio_codec_options) {
VALIDATE_STRING(params->audio_codec_options);
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
}
if (params->video_encoder) {
VALIDATE_STRING(params->video_encoder);
ADD_PARAM("video_encoder=%s", params->video_encoder);
}
if (params->audio_encoder) {
VALIDATE_STRING(params->audio_encoder);
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
}
if (params->power_off_on_close) {
@ -607,6 +659,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
}
}
if (control_socket != SC_SOCKET_NONE) {
// Disable Nagle's algorithm for the control socket
// (it only impacts the sending side, so it is useless to set it
// for the other sockets)
bool ok = net_set_tcp_nodelay(control_socket, true);
(void) ok; // error already logged
}
// we don't need the adb tunnel anymore
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);

View File

@ -44,12 +44,13 @@ struct sc_server_params {
uint16_t max_size;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint16_t max_fps;
const char *max_fps; // float to be parsed by the server
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool video;
bool audio;
bool audio_dup;
bool show_touches;
bool stay_awake;
bool force_adb_forward;

View File

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

View File

@ -0,0 +1,50 @@
#ifndef SC_GAMEPAD_PROCESSOR_H
#define SC_GAMEPAD_PROCESSOR_H
#include "common.h"
#include <assert.h>
#include <stdbool.h>
#include "input_events.h"
/**
* Gamepad processor trait.
*
* Component able to handle gamepads devices and inject buttons and axis events.
*/
struct sc_gamepad_processor {
const struct sc_gamepad_processor_ops *ops;
};
struct sc_gamepad_processor_ops {
/**
* Process a gamepad device added or removed
*
* This function is mandatory.
*/
void
(*process_gamepad_device)(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event);
/**
* Process a gamepad axis event
*
* This function is mandatory.
*/
void
(*process_gamepad_axis)(struct sc_gamepad_processor *gp,
const struct sc_gamepad_axis_event *event);
/**
* Process a gamepad button event
*
* This function is mandatory.
*/
void
(*process_gamepad_button)(struct sc_gamepad_processor *gp,
const struct sc_gamepad_button_event *event);
};
#endif

123
app/src/uhid/gamepad_uhid.c Normal file
View File

@ -0,0 +1,123 @@
#include "gamepad_uhid.h"
#include "hid/hid_gamepad.h"
#include "input_events.h"
#include "util/log.h"
/** Downcast gamepad processor to sc_gamepad_uhid */
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor)
static void
sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_input *hid_input,
const char *name) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = hid_input->hid_id;
assert(hid_input->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, hid_input->data, hid_input->size);
msg.uhid_input.size = hid_input->size;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_INPUT message (%s)", name);
}
}
static void
sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_open *hid_open) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE;
msg.uhid_create.id = hid_open->hid_id;
msg.uhid_create.name = hid_open->name;
msg.uhid_create.report_desc = hid_open->report_desc;
msg.uhid_create.report_desc_size = hid_open->report_desc_size;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_CREATE message (gamepad)");
}
}
static void
sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad,
const struct sc_hid_close *hid_close) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY;
msg.uhid_create.id = hid_close->hid_id;
if (!sc_controller_push_msg(gamepad->controller, &msg)) {
LOGE("Could not push UHID_DESTROY message (gamepad)");
}
}
static void
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
struct sc_hid_open hid_open;
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
event->gamepad_id)) {
return;
}
sc_gamepad_uhid_send_open(gamepad, &hid_open);
} else {
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
struct sc_hid_close hid_close;
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
event->gamepad_id)) {
return;
}
sc_gamepad_uhid_send_close(gamepad, &hid_close);
}
}
static void
sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp,
const struct sc_gamepad_axis_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input,
event)) {
return;
}
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad axis");
}
static void
sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp,
const struct sc_gamepad_button_event *event) {
struct sc_gamepad_uhid *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input,
event)) {
return;
}
sc_gamepad_uhid_send_input(gamepad, &hid_input, "gamepad button");
}
void
sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad,
struct sc_controller *controller) {
sc_hid_gamepad_init(&gamepad->hid);
gamepad->controller = controller;
static const struct sc_gamepad_processor_ops ops = {
.process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
};
gamepad->gamepad_processor.ops = &ops;
}

View File

@ -0,0 +1,23 @@
#ifndef SC_GAMEPAD_UHID_H
#define SC_GAMEPAD_UHID_H
#include "common.h"
#include <stdbool.h>
#include "controller.h"
#include "hid/hid_gamepad.h"
#include "trait/gamepad_processor.h"
struct sc_gamepad_uhid {
struct sc_gamepad_processor gamepad_processor; // gamepad processor trait
struct sc_hid_gamepad hid;
struct sc_controller *controller;
};
void
sc_gamepad_uhid_init(struct sc_gamepad_uhid *mouse,
struct sc_controller *controller);
#endif

View File

@ -9,21 +9,19 @@
#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) {
const struct sc_hid_input *hid_input) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = UHID_KEYBOARD_ID;
msg.uhid_input.id = hid_input->hid_id;
assert(event->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, event->data, event->size);
msg.uhid_input.size = event->size;
assert(hid_input->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, hid_input->data, hid_input->size);
msg.uhid_input.size = hid_input->size;
if (!sc_controller_push_msg(kb->controller, &msg)) {
LOGE("Could not send UHID_INPUT message (key)");
LOGE("Could not push UHID_INPUT message (key)");
}
}
@ -31,23 +29,22 @@ 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;
uint16_t diff = mod ^ kb->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);
kb->device_mod = mod;
struct sc_hid_event hid_event;
sc_hid_keyboard_event_from_mods(&hid_event, diff);
struct sc_hid_input hid_input;
if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, diff)) {
return;
}
LOGV("HID keyboard state synchronized");
sc_keyboard_uhid_send_input(kb, &hid_event);
sc_keyboard_uhid_send_input(kb, &hid_input);
}
}
@ -57,6 +54,8 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
uint64_t ack_to_wait) {
(void) ack_to_wait;
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
if (event->repeat) {
// In USB HID protocol, key repeat is handled by the host (Android), so
// just ignore key repeat here.
@ -65,22 +64,20 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
struct sc_keyboard_uhid *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
struct sc_hid_input hid_input;
// Not all keys are supported, just ignore unsupported keys
if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) {
if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) {
if (event->scancode == SC_SCANCODE_CAPSLOCK) {
atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_CAPS,
memory_order_relaxed);
kb->device_mod ^= SC_MOD_CAPS;
} else if (event->scancode == SC_SCANCODE_NUMLOCK) {
atomic_fetch_xor_explicit(&kb->device_mod, SC_MOD_NUM,
memory_order_relaxed);
kb->device_mod ^= SC_MOD_NUM;
} 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);
sc_keyboard_uhid_send_input(kb, &hid_input);
}
}
@ -98,34 +95,31 @@ sc_keyboard_uhid_to_sc_mod(uint8_t hid_led) {
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
void
sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb,
const uint8_t *data, size_t size) {
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
assert(len);
assert(size);
// Also check at runtime (do not trust the server)
if (!len) {
if (!size) {
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);
kb->device_mod = device_mod;
}
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller,
struct sc_uhid_devices *uhid_devices) {
struct sc_controller *controller) {
sc_hid_keyboard_init(&kb->hid);
kb->controller = controller;
atomic_init(&kb->device_mod, 0);
kb->device_mod = 0;
static const struct sc_key_processor_ops ops = {
.process_key = sc_key_processor_process_key,
@ -140,19 +134,16 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
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_hid_open hid_open;
sc_hid_keyboard_generate_open(&hid_open);
assert(hid_open.hid_id == SC_HID_ID_KEYBOARD);
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;
msg.uhid_create.id = SC_HID_ID_KEYBOARD;
msg.uhid_create.name = hid_open.name;
msg.uhid_create.report_desc = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) {
LOGE("Could not send UHID_CREATE message (keyboard)");
return false;

View File

@ -7,21 +7,22 @@
#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;
uint16_t device_mod;
};
bool
sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb,
struct sc_controller *controller,
struct sc_uhid_devices *uhid_devices);
struct sc_controller *controller);
void
sc_keyboard_uhid_process_hid_output(struct sc_keyboard_uhid *kb,
const uint8_t *data, size_t size);
#endif

View File

@ -7,21 +7,20 @@
/** 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) {
const struct sc_hid_input *hid_input,
const char *name) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_UHID_INPUT;
msg.uhid_input.id = UHID_MOUSE_ID;
msg.uhid_input.id = hid_input->hid_id;
assert(event->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, event->data, event->size);
msg.uhid_input.size = event->size;
assert(hid_input->size <= SC_HID_MAX_SIZE);
memcpy(msg.uhid_input.data, hid_input->data, hid_input->size);
msg.uhid_input.size = hid_input->size;
if (!sc_controller_push_msg(mouse->controller, &msg)) {
LOGE("Could not send UHID_INPUT message (%s)", name);
LOGE("Could not push UHID_INPUT message (%s)", name);
}
}
@ -30,10 +29,10 @@ 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_motion(&hid_input, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse motion");
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse motion");
}
static void
@ -41,10 +40,10 @@ 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_click(&hid_input, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse click");
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse click");
}
static void
@ -52,10 +51,10 @@ 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
sc_mouse_uhid_send_input(mouse, &hid_event, "mouse scroll");
sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll");
}
bool
@ -75,13 +74,18 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse,
mouse->mouse_processor.relative_mode = true;
struct sc_hid_open hid_open;
sc_hid_mouse_generate_open(&hid_open);
assert(hid_open.hid_id == SC_HID_ID_MOUSE);
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;
msg.uhid_create.id = SC_HID_ID_MOUSE;
msg.uhid_create.name = hid_open.name;
msg.uhid_create.report_desc = hid_open.report_desc;
msg.uhid_create.report_desc_size = hid_open.report_desc_size;
if (!sc_controller_push_msg(controller, &msg)) {
LOGE("Could not send UHID_CREATE message (mouse)");
LOGE("Could not push UHID_CREATE message (mouse)");
return false;
}

View File

@ -1,25 +1,27 @@
#include "uhid_output.h"
#include <assert.h>
#include <inttypes.h>
#include "uhid/keyboard_uhid.h"
#include "util/log.h"
void
sc_uhid_devices_init(struct sc_uhid_devices *devices) {
devices->count = 0;
sc_uhid_devices_init(struct sc_uhid_devices *devices,
struct sc_keyboard_uhid *keyboard) {
devices->keyboard = keyboard;
}
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];
sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id,
const uint8_t *data, size_t size) {
if (id == SC_HID_ID_KEYBOARD) {
if (devices->keyboard) {
sc_keyboard_uhid_process_hid_output(devices->keyboard, data, size);
} else {
LOGW("Unexpected keyboard HID output without UHID keyboard");
}
} else {
LOGW("HID output ignored for id %" PRIu16, id);
}
return NULL;
}

View File

@ -9,37 +9,19 @@
/**
* 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).
* This component dispatches HID outputs to the expected processor.
*/
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;
struct sc_keyboard_uhid *keyboard;
};
void
sc_uhid_devices_init(struct sc_uhid_devices *devices);
sc_uhid_devices_init(struct sc_uhid_devices *devices,
struct sc_keyboard_uhid *keyboard);
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);
sc_uhid_devices_process_hid_output(struct sc_uhid_devices *devices, uint16_t id,
const uint8_t *data, size_t size);
#endif

View File

@ -1,11 +1,14 @@
#include "util/log.h"
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include "aoa_hid.h"
#include "events.h"
#include "util/log.h"
#include "util/str.h"
#include "util/vector.h"
// See <https://source.android.com/devices/accessories/aoa2#hid-support>.
#define ACCESSORY_REGISTER_HID 54
@ -15,26 +18,49 @@
#define DEFAULT_TIMEOUT 1000
#define SC_AOA_EVENT_QUEUE_MAX 64
// Drop droppable events above this limit
#define SC_AOA_EVENT_QUEUE_LIMIT 60
struct sc_vec_hid_ids SC_VECTOR(uint16_t);
static void
sc_hid_event_log(uint16_t accessory_id, const struct sc_hid_event *event) {
// HID Event: [00] FF FF FF FF...
assert(event->size);
char *hex = sc_str_to_hex_string(event->data, event->size);
sc_hid_input_log(const struct sc_hid_input *hid_input) {
// HID input: [00] FF FF FF FF...
assert(hid_input->size);
char *hex = sc_str_to_hex_string(hid_input->data, hid_input->size);
if (!hex) {
return;
}
LOGV("HID Event: [%d] %s", accessory_id, hex);
LOGV("HID input: [%" PRIu16 "] %s", hid_input->hid_id, hex);
free(hex);
}
static void
sc_hid_open_log(const struct sc_hid_open *hid_open) {
// HID open: [00] FF FF FF FF...
assert(hid_open->report_desc_size);
char *hex = sc_str_to_hex_string(hid_open->report_desc,
hid_open->report_desc_size);
if (!hex) {
return;
}
LOGV("HID open: [%" PRIu16 "] %s", hid_open->hid_id, hex);
free(hex);
}
static void
sc_hid_close_log(const struct sc_hid_close *hid_close) {
// HID close: [00]
LOGV("HID close: [%" PRIu16 "]", hid_close->hid_id);
}
bool
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
struct sc_acksync *acksync) {
sc_vecdeque_init(&aoa->queue);
if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_MAX)) {
// Add 4 to support 4 non-droppable events without re-allocation
if (!sc_vecdeque_reserve(&aoa->queue, SC_AOA_EVENT_QUEUE_LIMIT + 4)) {
return false;
}
@ -125,38 +151,18 @@ sc_aoa_set_hid_report_desc(struct sc_aoa *aoa, uint16_t accessory_id,
return true;
}
bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const uint8_t *report_desc, uint16_t report_desc_size) {
bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size);
if (!ok) {
return false;
}
ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc,
report_desc_size);
if (!ok) {
if (!sc_aoa_unregister_hid(aoa, accessory_id)) {
LOGW("Could not unregister HID");
}
return false;
}
return true;
}
static bool
sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id,
const struct sc_hid_event *event) {
sc_aoa_send_hid_event(struct sc_aoa *aoa,
const struct sc_hid_input *hid_input) {
uint8_t request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR;
uint8_t request = ACCESSORY_SEND_HID_EVENT;
// <https://source.android.com/devices/accessories/aoa2.html#hid-support>
// value (arg0): accessory assigned ID for the HID device
// index (arg1): 0 (unused)
uint16_t value = accessory_id;
uint16_t value = hid_input->hid_id;
uint16_t index = 0;
unsigned char *data = (uint8_t *) event->data; // discard const
uint16_t length = event->size;
unsigned char *data = (uint8_t *) hid_input->data; // discard const
uint16_t length = hid_input->size;
int result = libusb_control_transfer(aoa->usb->handle, request_type,
request, value, index, data, length,
DEFAULT_TIMEOUT);
@ -169,7 +175,7 @@ sc_aoa_send_hid_event(struct sc_aoa *aoa, uint16_t accessory_id,
return true;
}
bool
static bool
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 = ACCESSORY_UNREGISTER_HID;
@ -192,41 +198,213 @@ sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id) {
return true;
}
static bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const uint8_t *report_desc, uint16_t report_desc_size) {
bool ok = sc_aoa_register_hid(aoa, accessory_id, report_desc_size);
if (!ok) {
return false;
}
ok = sc_aoa_set_hid_report_desc(aoa, accessory_id, report_desc,
report_desc_size);
if (!ok) {
if (!sc_aoa_unregister_hid(aoa, accessory_id)) {
LOGW("Could not unregister HID");
}
return false;
}
return true;
}
bool
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) {
sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa,
const struct sc_hid_input *hid_input,
uint64_t ack_to_wait) {
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
sc_hid_event_log(accessory_id, event);
sc_hid_input_log(hid_input);
}
sc_mutex_lock(&aoa->mutex);
bool full = sc_vecdeque_is_full(&aoa->queue);
if (!full) {
bool pushed = false;
size_t size = sc_vecdeque_size(&aoa->queue);
if (size < SC_AOA_EVENT_QUEUE_LIMIT) {
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
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;
aoa_event->type = SC_AOA_EVENT_TYPE_INPUT;
aoa_event->input.hid = *hid_input;
aoa_event->input.ack_to_wait = ack_to_wait;
pushed = true;
if (was_empty) {
sc_cond_signal(&aoa->event_cond);
}
}
// Otherwise (if the queue is full), the event is discarded
// Otherwise, the event is discarded
sc_mutex_unlock(&aoa->mutex);
return !full;
return pushed;
}
bool
sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open,
bool exit_on_open_error) {
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
sc_hid_open_log(hid_open);
}
sc_mutex_lock(&aoa->mutex);
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
// an OPEN event is non-droppable, so push it to the queue even above the
// SC_AOA_EVENT_QUEUE_LIMIT
struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue);
if (!aoa_event) {
LOG_OOM();
sc_mutex_unlock(&aoa->mutex);
return false;
}
aoa_event->type = SC_AOA_EVENT_TYPE_OPEN;
aoa_event->open.hid = *hid_open;
aoa_event->open.exit_on_error = exit_on_open_error;
if (was_empty) {
sc_cond_signal(&aoa->event_cond);
}
sc_mutex_unlock(&aoa->mutex);
return true;
}
bool
sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close) {
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
sc_hid_close_log(hid_close);
}
sc_mutex_lock(&aoa->mutex);
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
// a CLOSE event is non-droppable, so push it to the queue even above the
// SC_AOA_EVENT_QUEUE_LIMIT
struct sc_aoa_event *aoa_event = sc_vecdeque_push_hole(&aoa->queue);
if (!aoa_event) {
LOG_OOM();
sc_mutex_unlock(&aoa->mutex);
return false;
}
aoa_event->type = SC_AOA_EVENT_TYPE_CLOSE;
aoa_event->close.hid = *hid_close;
if (was_empty) {
sc_cond_signal(&aoa->event_cond);
}
sc_mutex_unlock(&aoa->mutex);
return true;
}
static bool
sc_aoa_process_event(struct sc_aoa *aoa, struct sc_aoa_event *event,
struct sc_vec_hid_ids *vec_open) {
switch (event->type) {
case SC_AOA_EVENT_TYPE_INPUT: {
uint64_t ack_to_wait = event->input.ack_to_wait;
if (ack_to_wait != SC_SEQUENCE_INVALID) {
LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait);
// If some events have ack_to_wait set, then sc_aoa must have
// been initialized with a non NULL acksync
assert(aoa->acksync);
// Do not block the loop indefinitely if the ack never comes (it
// should never happen)
sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500);
enum sc_acksync_wait_result result =
sc_acksync_wait(aoa->acksync, ack_to_wait, deadline);
if (result == SC_ACKSYNC_WAIT_TIMEOUT) {
LOGW("Ack not received after 500ms, discarding HID event");
// continue to process events
return true;
} else if (result == SC_ACKSYNC_WAIT_INTR) {
// stopped
return false;
}
}
struct sc_hid_input *hid_input = &event->input.hid;
bool ok = sc_aoa_send_hid_event(aoa, hid_input);
if (!ok) {
LOGW("Could not send HID event to USB device: %" PRIu16,
hid_input->hid_id);
}
break;
}
case SC_AOA_EVENT_TYPE_OPEN: {
struct sc_hid_open *hid_open = &event->open.hid;
bool ok = sc_aoa_setup_hid(aoa, hid_open->hid_id,
hid_open->report_desc,
hid_open->report_desc_size);
if (ok) {
// The device is now open, add it to the list of devices to
// close automatically on exit
bool pushed = sc_vector_push(vec_open, hid_open->hid_id);
if (!pushed) {
LOG_OOM();
// this is not fatal, the HID device will just not be
// explicitly unregistered
}
} else {
LOGW("Could not open AOA device: %" PRIu16, hid_open->hid_id);
if (event->open.exit_on_error) {
// Notify the error to the main thread, which will exit
sc_push_event(SC_EVENT_AOA_OPEN_ERROR);
}
}
break;
}
case SC_AOA_EVENT_TYPE_CLOSE: {
struct sc_hid_close *hid_close = &event->close.hid;
bool ok = sc_aoa_unregister_hid(aoa, hid_close->hid_id);
if (ok) {
// The device is not open anymore, remove it from the list of
// devices to close automatically on exit
ssize_t idx = sc_vector_index_of(vec_open, hid_close->hid_id);
if (idx >= 0) {
sc_vector_remove(vec_open, idx);
}
} else {
LOGW("Could not close AOA device: %" PRIu16, hid_close->hid_id);
}
break;
}
}
// continue to process events
return true;
}
static int
run_aoa_thread(void *data) {
struct sc_aoa *aoa = data;
// Store the HID ids of opened devices to unregister them all before exiting
struct sc_vec_hid_ids vec_open = SC_VECTOR_INITIALIZER;
for (;;) {
sc_mutex_lock(&aoa->mutex);
while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) {
@ -240,36 +418,26 @@ run_aoa_thread(void *data) {
assert(!sc_vecdeque_is_empty(&aoa->queue));
struct sc_aoa_event event = sc_vecdeque_pop(&aoa->queue);
uint64_t ack_to_wait = event.ack_to_wait;
sc_mutex_unlock(&aoa->mutex);
if (ack_to_wait != SC_SEQUENCE_INVALID) {
LOGD("Waiting ack from server sequence=%" PRIu64_, ack_to_wait);
// If some events have ack_to_wait set, then sc_aoa must have been
// initialized with a non NULL acksync
assert(aoa->acksync);
// Do not block the loop indefinitely if the ack never comes (it
// should never happen)
sc_tick deadline = sc_tick_now() + SC_TICK_FROM_MS(500);
enum sc_acksync_wait_result result =
sc_acksync_wait(aoa->acksync, ack_to_wait, deadline);
if (result == SC_ACKSYNC_WAIT_TIMEOUT) {
LOGW("Ack not received after 500ms, discarding HID event");
continue;
} else if (result == SC_ACKSYNC_WAIT_INTR) {
// stopped
break;
}
}
bool ok = sc_aoa_send_hid_event(aoa, event.accessory_id, &event.hid);
if (!ok) {
LOGW("Could not send HID event to USB device");
bool cont = sc_aoa_process_event(aoa, &event, &vec_open);
if (!cont) {
// stopped
break;
}
}
// Explicitly unregister all registered HID ids before exiting
for (size_t i = 0; i < vec_open.size; ++i) {
uint16_t hid_id = vec_open.data[i];
LOGD("Unregistering AOA device %" PRIu16 "...", hid_id);
bool ok = sc_aoa_unregister_hid(aoa, hid_id);
if (!ok) {
LOGW("Could not close AOA device: %" PRIu16, hid_id);
}
}
sc_vector_destroy(&vec_open);
return 0;
}

View File

@ -13,12 +13,27 @@
#include "util/tick.h"
#include "util/vecdeque.h"
#define SC_HID_MAX_SIZE 8
enum sc_aoa_event_type {
SC_AOA_EVENT_TYPE_OPEN,
SC_AOA_EVENT_TYPE_INPUT,
SC_AOA_EVENT_TYPE_CLOSE,
};
struct sc_aoa_event {
struct sc_hid_event hid;
uint16_t accessory_id;
uint64_t ack_to_wait;
enum sc_aoa_event_type type;
union {
struct {
struct sc_hid_open hid;
bool exit_on_error;
} open;
struct {
struct sc_hid_close hid;
} close;
struct {
struct sc_hid_input hid;
uint64_t ack_to_wait;
} input;
};
};
struct sc_aoa_event_queue SC_VECDEQUE(struct sc_aoa_event);
@ -49,24 +64,31 @@ sc_aoa_stop(struct sc_aoa *aoa);
void
sc_aoa_join(struct sc_aoa *aoa);
//bool
//sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
// const uint8_t *report_desc, uint16_t report_desc_size);
//
//bool
//sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id);
// report_desc must be a pointer to static memory, accessed at any time from
// another thread
bool
sc_aoa_setup_hid(struct sc_aoa *aoa, uint16_t accessory_id,
const uint8_t *report_desc, uint16_t report_desc_size);
sc_aoa_push_open(struct sc_aoa *aoa, const struct sc_hid_open *hid_open,
bool exit_on_open_error);
bool
sc_aoa_unregister_hid(struct sc_aoa *aoa, uint16_t accessory_id);
sc_aoa_push_close(struct sc_aoa *aoa, const struct sc_hid_close *hid_close);
bool
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);
sc_aoa_push_input_with_ack_to_wait(struct sc_aoa *aoa,
const struct sc_hid_input *hid_input,
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);
sc_aoa_push_input(struct sc_aoa *aoa, const struct sc_hid_input *hid_input) {
return sc_aoa_push_input_with_ack_to_wait(aoa, hid_input,
SC_SEQUENCE_INVALID);
}
#endif

91
app/src/usb/gamepad_aoa.c Normal file
View File

@ -0,0 +1,91 @@
#include "gamepad_aoa.h"
#include "input_events.h"
#include "util/log.h"
/** Downcast gamepad processor to gamepad_aoa */
#define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor)
static void
sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp,
const struct sc_gamepad_device_event *event) {
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
if (event->type == SC_GAMEPAD_DEVICE_ADDED) {
struct sc_hid_open hid_open;
if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open,
event->gamepad_id)) {
return;
}
// exit_on_error: false (a gamepad open failure should not exit scrcpy)
if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) {
LOGW("Could not push AOA HID open (gamepad)");
}
} else {
assert(event->type == SC_GAMEPAD_DEVICE_REMOVED);
struct sc_hid_close hid_close;
if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close,
event->gamepad_id)) {
return;
}
if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) {
LOGW("Could not push AOA HID close (gamepad)");
}
}
}
static void
sc_gamepad_processor_process_gamepad_axis(struct sc_gamepad_processor *gp,
const struct sc_gamepad_axis_event *event) {
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
if (!sc_hid_gamepad_generate_input_from_axis(&gamepad->hid, &hid_input,
event)) {
return;
}
if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (gamepad axis)");
}
}
static void
sc_gamepad_processor_process_gamepad_button(struct sc_gamepad_processor *gp,
const struct sc_gamepad_button_event *event) {
struct sc_gamepad_aoa *gamepad = DOWNCAST(gp);
struct sc_hid_input hid_input;
if (!sc_hid_gamepad_generate_input_from_button(&gamepad->hid, &hid_input,
event)) {
return;
}
if (!sc_aoa_push_input(gamepad->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (gamepad button)");
}
}
void
sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) {
gamepad->aoa = aoa;
sc_hid_gamepad_init(&gamepad->hid);
static const struct sc_gamepad_processor_ops ops = {
.process_gamepad_device = sc_gamepad_processor_process_gamepad_device,
.process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis,
.process_gamepad_button = sc_gamepad_processor_process_gamepad_button,
};
gamepad->gamepad_processor.ops = &ops;
}
void
sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad) {
(void) gamepad;
// Do nothing, gamepad->aoa will automatically unregister all devices
}

25
app/src/usb/gamepad_aoa.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef SC_GAMEPAD_AOA_H
#define SC_GAMEPAD_AOA_H
#include "common.h"
#include <stdbool.h>
#include "aoa_hid.h"
#include "hid/hid_gamepad.h"
#include "trait/gamepad_processor.h"
struct sc_gamepad_aoa {
struct sc_gamepad_processor gamepad_processor; // gamepad processor trait
struct sc_hid_gamepad hid;
struct sc_aoa *aoa;
};
void
sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa);
void
sc_gamepad_aoa_destroy(struct sc_gamepad_aoa *gamepad);
#endif

View File

@ -8,19 +8,16 @@
/** 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)) {
struct sc_hid_input hid_input;
if (!sc_hid_keyboard_generate_input_from_mods(&hid_input, 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)");
if (!sc_aoa_push_input(kb->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mod lock state)");
return false;
}
@ -41,10 +38,10 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
struct sc_keyboard_aoa *kb = DOWNCAST(kp);
struct sc_hid_event hid_event;
struct sc_hid_input hid_input;
// Not all keys are supported, just ignore unsupported keys
if (sc_hid_keyboard_event_from_key(&kb->hid, &hid_event, event)) {
if (sc_hid_keyboard_generate_input_from_key(&kb->hid, &hid_input, event)) {
if (!kb->mod_lock_synchronized) {
// Inject CAPSLOCK and/or NUMLOCK if necessary to synchronize
// keyboard state
@ -58,11 +55,9 @@ sc_key_processor_process_key(struct sc_key_processor *kp,
// 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)");
if (!sc_aoa_push_input_with_ack_to_wait(kb->aoa, &hid_input,
ack_to_wait)) {
LOGW("Could not push AOA HID input (key)");
}
}
}
@ -71,11 +66,12 @@ 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);
struct sc_hid_open hid_open;
sc_hid_keyboard_generate_open(&hid_open);
bool ok = sc_aoa_push_open(aoa, &hid_open, true);
if (!ok) {
LOGW("Register HID keyboard failed");
LOGW("Could not push AOA HID open (keyboard)");
return false;
}
@ -102,9 +98,6 @@ sc_keyboard_aoa_init(struct sc_keyboard_aoa *kb, struct sc_aoa *aoa) {
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");
}
(void) kb;
// Do nothing, kb->aoa will automatically unregister all devices
}

View File

@ -9,19 +9,16 @@
/** 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_motion(&hid_input, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse motion)");
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse motion)");
}
}
@ -30,12 +27,11 @@ 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_click(&hid_input, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse click)");
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse click)");
}
}
@ -44,12 +40,11 @@ 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);
struct sc_hid_input hid_input;
sc_hid_mouse_generate_input_from_scroll(&hid_input, event);
if (!sc_aoa_push_hid_event(mouse->aoa, HID_MOUSE_ACCESSORY_ID,
&hid_event)) {
LOGW("Could not request HID event (mouse scroll)");
if (!sc_aoa_push_input(mouse->aoa, &hid_input)) {
LOGW("Could not push AOA HID input (mouse scroll)");
}
}
@ -57,11 +52,12 @@ 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);
struct sc_hid_open hid_open;
sc_hid_mouse_generate_open(&hid_open);
bool ok = sc_aoa_push_open(aoa, &hid_open, true);
if (!ok) {
LOGW("Register HID mouse failed");
LOGW("Could not push AOA HID open (mouse)");
return false;
}
@ -82,8 +78,6 @@ sc_mouse_aoa_init(struct sc_mouse_aoa *mouse, struct sc_aoa *aoa) {
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");
}
(void) mouse;
// Do nothing, mouse->aoa will automatically unregister all devices
}

View File

@ -12,6 +12,7 @@ struct scrcpy_otg {
struct sc_aoa aoa;
struct sc_keyboard_aoa keyboard;
struct sc_mouse_aoa mouse;
struct sc_gamepad_aoa gamepad;
struct sc_screen_otg screen_otg;
};
@ -21,12 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
(void) usb;
(void) userdata;
SDL_Event event;
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED;
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
}
sc_push_event(SC_EVENT_USB_DEVICE_DISCONNECTED);
}
static enum scrcpy_exit_code
@ -37,6 +33,9 @@ event_loop(struct scrcpy_otg *s) {
case SC_EVENT_USB_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_AOA_OPEN_ERROR:
LOGE("AOA open error");
return SCRCPY_EXIT_FAILURE;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
@ -59,12 +58,23 @@ scrcpy_otg(struct scrcpy_options *options) {
LOGW("Could not enable linear filtering");
}
if (!SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1")) {
LOGW("Could not allow joystick background events");
}
// Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
return SCRCPY_EXIT_FAILURE;
}
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
if (SDL_Init(SDL_INIT_GAMECONTROLLER)) {
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
// Not fatal, keyboard/mouse should still work
}
}
atexit(SDL_Quit);
if (!SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")) {
@ -75,6 +85,7 @@ scrcpy_otg(struct scrcpy_options *options) {
struct sc_keyboard_aoa *keyboard = NULL;
struct sc_mouse_aoa *mouse = NULL;
struct sc_gamepad_aoa *gamepad = NULL;
bool usb_device_initialized = false;
bool usb_connected = false;
bool aoa_started = false;
@ -121,11 +132,15 @@ scrcpy_otg(struct scrcpy_options *options) {
|| options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_DISABLED);
assert(options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA
|| options->mouse_input_mode == SC_MOUSE_INPUT_MODE_DISABLED);
assert(options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA
|| options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_DISABLED);
bool enable_keyboard =
options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA;
bool enable_mouse =
options->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA;
bool enable_gamepad =
options->gamepad_input_mode == SC_GAMEPAD_INPUT_MODE_AOA;
if (enable_keyboard) {
ok = sc_keyboard_aoa_init(&s->keyboard, &s->aoa);
@ -143,6 +158,11 @@ scrcpy_otg(struct scrcpy_options *options) {
mouse = &s->mouse;
}
if (enable_gamepad) {
sc_gamepad_aoa_init(&s->gamepad, &s->aoa);
gamepad = &s->gamepad;
}
ok = sc_aoa_start(&s->aoa);
if (!ok) {
goto end;
@ -157,6 +177,7 @@ scrcpy_otg(struct scrcpy_options *options) {
struct sc_screen_otg_params params = {
.keyboard = keyboard,
.mouse = mouse,
.gamepad = gamepad,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@ -190,6 +211,9 @@ end:
if (keyboard) {
sc_keyboard_aoa_destroy(&s->keyboard);
}
if (gamepad) {
sc_gamepad_aoa_destroy(&s->gamepad);
}
if (aoa_initialized) {
sc_aoa_join(&s->aoa);

View File

@ -59,6 +59,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
const struct sc_screen_otg_params *params) {
screen->keyboard = params->keyboard;
screen->mouse = params->mouse;
screen->gamepad = params->gamepad;
screen->mouse_capture_key_pressed = 0;
@ -169,7 +170,7 @@ sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen,
// .position not used for HID events
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state),
};
assert(mp->ops->process_mouse_motion);
@ -188,8 +189,7 @@ sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen,
// .position not used for HID events
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_click);
@ -208,14 +208,94 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
// .position not used for HID events
.hscroll = event->x,
.vscroll = event->y,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state, true),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_scroll);
mp->ops->process_mouse_scroll(mp, &evt);
}
static void
sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
const SDL_ControllerDeviceEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
SDL_JoystickID id;
if (event->type == SDL_CONTROLLERDEVICEADDED) {
SDL_GameController *gc = SDL_GameControllerOpen(event->which);
if (!gc) {
LOGW("Could not open game controller");
return;
}
SDL_Joystick *joystick = SDL_GameControllerGetJoystick(gc);
if (!joystick) {
LOGW("Could not get controller joystick");
SDL_GameControllerClose(gc);
return;
}
id = SDL_JoystickInstanceID(joystick);
} else if (event->type == SDL_CONTROLLERDEVICEREMOVED) {
id = event->which;
SDL_GameController *gc = SDL_GameControllerFromInstanceID(id);
if (gc) {
SDL_GameControllerClose(gc);
} else {
LOGW("Unknown gamepad device removed");
}
} else {
// Nothing to do
return;
}
struct sc_gamepad_device_event evt = {
.type = sc_gamepad_device_event_type_from_sdl_type(event->type),
.gamepad_id = id,
};
gp->ops->process_gamepad_device(gp, &evt);
}
static void
sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
const SDL_ControllerAxisEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
}
struct sc_gamepad_axis_event evt = {
.gamepad_id = event->which,
.axis = axis,
.value = event->value,
};
gp->ops->process_gamepad_axis(gp, &evt);
}
static void
sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
const SDL_ControllerButtonEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
}
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_controllerbutton_type(event->type),
.button = button,
};
gp->ops->process_gamepad_button(gp, &evt);
}
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
switch (event->type) {
@ -295,5 +375,23 @@ sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;
case SDL_CONTROLLERDEVICEADDED:
case SDL_CONTROLLERDEVICEREMOVED:
// Handle device added or removed even if paused
if (screen->gamepad) {
sc_screen_otg_process_gamepad_device(screen, &event->cdevice);
}
break;
case SDL_CONTROLLERAXISMOTION:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_axis(screen, &event->caxis);
}
break;
case SDL_CONTROLLERBUTTONDOWN:
case SDL_CONTROLLERBUTTONUP:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_button(screen, &event->cbutton);
}
break;
}
}

View File

@ -8,10 +8,12 @@
#include "keyboard_aoa.h"
#include "mouse_aoa.h"
#include "gamepad_aoa.h"
struct sc_screen_otg {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
SDL_Window *window;
SDL_Renderer *renderer;
@ -24,6 +26,7 @@ struct sc_screen_otg {
struct sc_screen_otg_params {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
const char *window_title;
bool always_on_top;

View File

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

View File

@ -13,6 +13,12 @@ sc_write16be(uint8_t *buf, uint16_t value) {
buf[1] = value;
}
static inline void
sc_write16le(uint8_t *buf, uint16_t value) {
buf[0] = value;
buf[1] = value >> 8;
}
static inline void
sc_write32be(uint8_t *buf, uint32_t value) {
buf[0] = value >> 24;
@ -21,12 +27,26 @@ sc_write32be(uint8_t *buf, uint32_t value) {
buf[3] = value;
}
static inline void
sc_write32le(uint8_t *buf, uint32_t value) {
buf[0] = value;
buf[1] = value >> 8;
buf[2] = value >> 16;
buf[3] = value >> 24;
}
static inline void
sc_write64be(uint8_t *buf, uint64_t value) {
sc_write32be(buf, value >> 32);
sc_write32be(&buf[4], (uint32_t) value);
}
static inline void
sc_write64le(uint8_t *buf, uint64_t value) {
sc_write32le(buf, (uint32_t) value);
sc_write32le(&buf[4], value >> 32);
}
static inline uint16_t
sc_read16be(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];

View File

@ -15,6 +15,7 @@
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <netinet/tcp.h>
# include <arpa/inet.h>
# include <unistd.h>
# include <fcntl.h>
@ -273,6 +274,22 @@ net_close(sc_socket socket) {
#endif
}
bool
net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay) {
sc_raw_socket raw_sock = unwrap(socket);
int value = tcp_nodelay ? 1 : 0;
int ret = setsockopt(raw_sock, IPPROTO_TCP, TCP_NODELAY,
(const void *) &value, sizeof(value));
if (ret == -1) {
net_perror("setsockopt(TCP_NODELAY)");
return false;
}
assert(ret == 0);
return true;
}
bool
net_parse_ipv4(const char *s, uint32_t *ipv4) {
struct in_addr addr;

View File

@ -67,6 +67,10 @@ net_interrupt(sc_socket socket);
bool
net_close(sc_socket socket);
// Disable Nagle's algorithm (if tcp_nodelay is true)
bool
net_set_tcp_nodelay(sc_socket socket, bool tcp_nodelay);
/**
* Parse `ip` "xxx.xxx.xxx.xxx" to an IPv4 host representation
*/

View File

@ -6,6 +6,8 @@
#include "log.h"
sc_thread_id SC_MAIN_THREAD_ID;
bool
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
void *userdata) {

View File

@ -39,6 +39,8 @@ typedef struct sc_cond {
SDL_cond *cond;
} sc_cond;
extern sc_thread_id SC_MAIN_THREAD_ID;
bool
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
void *userdata);

View File

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

View File

@ -42,6 +42,44 @@ static void test_write64be(void) {
assert(buf[7] == 0xEF);
}
static void test_write16le(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
sc_write16le(buf, val);
assert(buf[0] == 0xCD);
assert(buf[1] == 0xAB);
}
static void test_write32le(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
sc_write32le(buf, val);
assert(buf[0] == 0x34);
assert(buf[1] == 0x12);
assert(buf[2] == 0xCD);
assert(buf[3] == 0xAB);
}
static void test_write64le(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
sc_write64le(buf, val);
assert(buf[0] == 0xEF);
assert(buf[1] == 0x90);
assert(buf[2] == 0x78);
assert(buf[3] == 0x56);
assert(buf[4] == 0x34);
assert(buf[5] == 0x12);
assert(buf[6] == 0xCD);
assert(buf[7] == 0xAB);
}
static void test_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
@ -108,6 +146,10 @@ int main(int argc, char *argv[]) {
test_read32be();
test_read64be();
test_write16le();
test_write32le();
test_write64le();
test_float_to_u16fp();
test_float_to_i16fp();
return 0;

View File

@ -78,7 +78,7 @@ static void test_options(void) {
assert(opts->video_bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen);
assert(opts->max_fps == 30);
assert(!strcmp(opts->max_fps, "30"));
assert(opts->max_size == 1024);
assert(opts->lock_video_orientation == 2);
assert(opts->port_range.first == 1234);
@ -124,32 +124,22 @@ static void test_options2(void) {
}
static void test_parse_shortcut_mods(void) {
struct sc_shortcut_mods mods;
uint8_t mods;
bool ok;
ok = sc_parse_shortcut_mods("lctrl", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == SC_SHORTCUT_MOD_LCTRL);
ok = sc_parse_shortcut_mods("lctrl+lalt", &mods);
assert(ok);
assert(mods.count == 1);
assert(mods.data[0] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_LALT));
assert(mods == SC_SHORTCUT_MOD_LCTRL);
ok = sc_parse_shortcut_mods("rctrl,lalt", &mods);
assert(ok);
assert(mods.count == 2);
assert(mods.data[0] == SC_SHORTCUT_MOD_RCTRL);
assert(mods.data[1] == SC_SHORTCUT_MOD_LALT);
assert(mods == (SC_SHORTCUT_MOD_RCTRL | SC_SHORTCUT_MOD_LALT));
ok = sc_parse_shortcut_mods("lsuper,rsuper+lalt,lctrl+rctrl+ralt", &mods);
ok = sc_parse_shortcut_mods("lsuper,rsuper,lctrl", &mods);
assert(ok);
assert(mods.count == 3);
assert(mods.data[0] == SC_SHORTCUT_MOD_LSUPER);
assert(mods.data[1] == (SC_SHORTCUT_MOD_RSUPER | SC_SHORTCUT_MOD_LALT));
assert(mods.data[2] == (SC_SHORTCUT_MOD_LCTRL | SC_SHORTCUT_MOD_RCTRL |
SC_SHORTCUT_MOD_RALT));
assert(mods == (SC_SHORTCUT_MOD_LSUPER
| SC_SHORTCUT_MOD_RSUPER
| SC_SHORTCUT_MOD_LCTRL));
ok = sc_parse_shortcut_mods("", &mods);
assert(!ok);

View File

@ -329,6 +329,7 @@ static void test_serialize_uhid_create(void) {
.type = SC_CONTROL_MSG_TYPE_UHID_CREATE,
.uhid_create = {
.id = 42,
.name = "ABC",
.report_desc_size = sizeof(report_desc),
.report_desc = report_desc,
},
@ -336,12 +337,14 @@ static void test_serialize_uhid_create(void) {
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 16);
assert(size == 20);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_CREATE,
0, 42, // id
0, 11, // size
3, // name size
65, 66, 67, // "ABC"
0, 11, // report desc size
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
};
assert(!memcmp(buf, expected, sizeof(expected)));
@ -370,6 +373,25 @@ static void test_serialize_uhid_input(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_uhid_destroy(void) {
struct sc_control_msg msg = {
.type = SC_CONTROL_MSG_TYPE_UHID_DESTROY,
.uhid_destroy = {
.id = 42,
},
};
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 3);
const uint8_t expected[] = {
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
0, 42, // id
};
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,
@ -405,6 +427,7 @@ int main(int argc, char *argv[]) {
test_serialize_rotate_device();
test_serialize_uhid_create();
test_serialize_uhid_input();
test_serialize_uhid_destroy();
test_serialize_open_hard_keyboard();
return 0;
}

View File

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

View File

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

View File

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

View File

@ -106,15 +106,6 @@ only inverts _x_.
This only works for the default mouse mode (`--mouse=sdk`).
## Right-click and middle-click
By default, right-click triggers BACK (or POWER on) and middle-click triggers
HOME. To disable these shortcuts and forward the clicks to the device instead:
```bash
scrcpy --forward-all-clicks
```
## File drop
### Install APK

58
doc/gamepad.md Normal file
View File

@ -0,0 +1,58 @@
# Gamepad
Several gamepad input modes are available:
- `--gamepad=disabled` (default)
- `--gamepad=uhid` (or `-G`): simulates physical HID gamepads using the UHID
kernel module on the device
- `--gamepad=aoa`: simulates physical HID gamepads using the AOAv2 protocol
## Physical gamepad simulation
Two modes allow to simulate physical HID gamepads on the device, one for each
physical gamepad plugged into the computer.
### UHID
This mode simulates physical HID gamepads using the [UHID] kernel module on the
device.
[UHID]: https://kernel.org/doc/Documentation/hid/uhid.txt
To enable UHID gamepads, use:
```bash
scrcpy --gamepad=uhid
scrcpy -G # short version
```
Note: UHID may not work on old Android versions due to permission errors.
### AOA
This mode simulates physical HID gamepads using the [AOAv2] protocol.
[AOAv2]: https://source.android.com/devices/accessories/aoa2#hid-support
To enable AOA gamepads, use:
```bash
scrcpy --gamepad=aoa
```
Contrary to the other mode, 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: For some reason, in this mode, Android detects multiple physical gamepads
as a single misbehaving one. Use UHID if you need multiple gamepads.
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_).

View File

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

View File

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

View File

@ -6,16 +6,18 @@ 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
[physical mouse]: mouse.md#physical-mouse-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.
[keyboard](keyboard.md#aoa), [mouse](mouse.md#aoa) and
[gamepad](gamepad.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. However, gamepads are disabled by default, so
`--gamepad=aoa` (or `-G` in OTG mode) must be explicitly 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.
Therefore, it is possible to run _scrcpy_ with only physical keyboard, mouse and
gamepad simulation, as if the computer keyboard, mouse and gamepads were plugged
directly to the device via an OTG cable.
To enable OTG mode:
@ -32,6 +34,13 @@ scrcpy --otg --keyboard=disabled
scrcpy --otg --mouse=disabled
```
and to enable gamepads:
```bash
scrcpy --otg --gamepad=aoa
scrcpy --otg -G # short version
```
It only works if the device is connected over USB.
## OTG issues on Windows
@ -50,9 +59,9 @@ 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
scrcpy --no-video --no-audio --keyboard=uhid --mouse=uhid --gamepad=uhid
scrcpy --no-video --no-audio -KMG # short version
scrcpy --no-video --no-audio --keyboard=aoa --mouse=aoa --gamepad=aoa
```
One benefit of UHID is that it also works wirelessly.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +0,0 @@
package com.genymobile.scrcpy;
public interface Codec {
enum Type {
VIDEO,
AUDIO,
}
Type getType();
int getId();
String getName();
String getMimeType();
}

View File

@ -1,33 +0,0 @@
package com.genymobile.scrcpy;
import android.net.LocalSocket;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public final class ControlChannel {
private final InputStream inputStream;
private final OutputStream outputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
public ControlChannel(LocalSocket controlSocket) throws IOException {
this.inputStream = controlSocket.getInputStream();
this.outputStream = controlSocket.getOutputStream();
}
public ControlMessage recv() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(inputStream);
msg = reader.next();
}
return msg;
}
public void send(DeviceMessage msg) throws IOException {
writer.writeTo(msg, outputStream);
}
}

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