Compare commits

..

128 Commits

Author SHA1 Message Date
fa6c5b5149 Upgrade FFmpeg (6.0) for Windows
Use the latest version (specifically built for scrcpy).

Refs <https://www.ffmpeg.org/download.html#release_6.0>
2023-02-28 12:35:08 +01:00
884997e854 Use minimal prebuilt FFmpeg for Windows
On the scrcpy-deps repo, I built FFmpeg 5.1.2 binaries for Windows with
only the features used by scrcpy.

For comparison, here are the sizes of the dll for FFmpeg 5.1.2:
 - before: 89M
 - after: 4.7M

It also allows to upgrade the old FFmpeg version (4.3.1) used for win32.

Refs <https://github.com/rom1v/scrcpy-deps>
Refs <https://github.com/Genymobile/scrcpy/issues/1753>
2023-02-28 12:35:08 +01:00
a0fa9967b8 Simplify libusb prebuilt scripts
In theory, include/ might be slightly different for win32 and win64
builds. Use each one separately to simplify.
2023-02-28 12:35:08 +01:00
260edc318f Add compat support for FFmpeg < 5.1
The new chlayout API has been introduced in FFmpeg 5.1. Use the old
channel_layout API on older versions.
2023-02-28 12:35:08 +01:00
e48098ec8d Add workaround to capture audio on Android 11
On Android 11, it is possible to start the capture only when the running
app is in foreground. But scrcpy is not an app, it's a Java application
started from shell.

As a workaround, start an existing Android shell existing activity just
to start the capture, then close it immediately.

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-28 12:35:08 +01:00
8fad02aafa Add audio player
Play the decoded audio using SDL.

The audio player frame sink receives the audio frames, resample them
and write them to a byte buffer (introduced by this commit).

On SDL audio callback (from an internal SDL thread), copy samples from
this byte buffer to the SDL audio buffer.

The byte buffer is protected by the SDL_AudioDeviceLock(), but it has
been designed so that the producer and the consumer may write and read
in parallel, provided that they don't access the same slices of the
ring-buffer buffer.

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-02-28 12:35:08 +01:00
bb935764ae Add two-step write feature to bytebuf
If there is exactly one producer, then it can assume that the remaining
space in the buffer will only increase until it write something.

This assumption may allow the producer to write to the buffer (up to a
known safe size) without any synchronization mechanism, thus allowing
to read and write different parts of the buffer in parallel.

The producer can then commit the write with lock held, and update its
knowledge of the safe empty remaining space.
2023-02-28 12:35:08 +01:00
fe127af72e Introduce bytebuf util
Add a ring-buffer for bytes. It will be useful for buffering audio.
2023-02-28 12:35:08 +01:00
8e32d15e6c Pass AVCodecContext to frame sinks
Frame consumers may need details about the frame format.
2023-02-28 12:35:08 +01:00
7f2989e1d5 Add an audio decoder 2023-02-28 12:35:08 +01:00
da57902dd5 Give a name to decoder instances
This will be useful in logs.
2023-02-28 12:35:08 +01:00
28701090f6 Rename decoder to video_decoder 2023-02-28 12:35:08 +01:00
c50dc53bc2 Log display sizes in display list
This is more convenient than just the display id alone.
2023-02-28 12:35:08 +01:00
30b8429752 Add --list-device-displays 2023-02-28 12:35:08 +01:00
38e5dafba6 Move log message helpers to LogUtils
This class will also contain other log helpers.
2023-02-28 12:35:08 +01:00
d60a502485 Quit on audio configuration failure
When audio capture fails on the device, scrcpy continue mirroring the
video stream. This allows to enable audio by default only when
supported.

However, if an audio configuration occurs (for example the user
explicitly selected an unknown audio encoder), this must be treated as
an error and scrcpy must exit.
2023-02-28 12:35:08 +01:00
55f4c42f19 Add --list-encoders
Add an option to list the device encoders properly.
2023-02-28 12:35:08 +01:00
7a9eefb04a Move await_for_server() logs
Print the logs on the caller side. This will allow to call the function
in another context without printing the logs.
2023-02-28 12:35:08 +01:00
e39ad0f695 Add --audio-encoder
Similar to --video-encoder, but for audio.
2023-02-28 12:35:08 +01:00
377b6f57c5 Extract unknown encoder error message
This will allow to reuse the same code for audio encoder selection.
2023-02-28 12:35:08 +01:00
aa8ed923f0 Add --audio-codec-options
Similar to --video-codec-options, but for audio.
2023-02-28 12:35:08 +01:00
13211f82a1 Extract application of codec options
This will allow to reuse the same code for audio codec options.
2023-02-28 12:35:08 +01:00
7f50ed2458 Add support for AAC audio codec
Add option --audio-codec=aac.
2023-02-28 12:35:08 +01:00
9187472014 Add --audio-codec
Introduce the selection mechanism. Alternative codecs will be added
later.
2023-02-28 12:35:08 +01:00
a331c2c653 Add --audio-bit-rate
Add an option to configure the audio bit-rate.
2023-02-28 12:35:08 +01:00
f75dc2e477 Disable MethodLength checkstyle on createOptions()
This method will grow as needed to initialize options.
2023-02-28 12:35:08 +01:00
8a5be9e2a6 Rename --encoder to --video-encoder
This prepares the introduction of --audio-encoder.
2023-02-28 12:35:08 +01:00
705d69aaea Rename --codec-options to --video-codec-options
This prepares the introduction of --audio-codec-options.
2023-02-28 12:35:08 +01:00
ad51a2b411 Rename --bit-rate to --video-bit-rate
This prepares the introduction of --audio-bit-rate.
2023-02-28 12:35:08 +01:00
7581dc10d3 Rename --codec to --video-codec
This prepares the introduction of --audio-codec.
2023-02-28 12:35:08 +01:00
68cd396e1f Remove default bit-rate on client side
If no bit-rate is passed, let the server use the default value (8Mbps).

This avoids to define a default value on both sides, and to pass the
default bit-rate as an argument when starting the server.
2023-02-28 12:35:08 +01:00
8dc1fd172a Record at least video packets on stop
If the recorder is stopped while it has not received any audio packet
yet, make sure the video stream is correctly recorded.
2023-02-28 12:35:08 +01:00
9c34c34e5d Disable audio before Android 11
The permission "android.permission.RECORD_AUDIO" has been added for
shell in Android 11.

Moreover, on lower versions, it may make the server segfault on the
device (happened on a Nexus 5 with Android 6.0.1).

Refs <4feeee8891%5E%21/>
2023-02-28 12:35:08 +01:00
0abb268432 Disable audio on initialization error
By default, audio is enabled (--no-audio must be explicitly passed to
disable it).

However, some devices may not support audio capture (typically devices
below Android 11, or Android 11 when the shell application is not
foreground on start).

In that case, make the server notify the client to dynamically disable
audio forwarding so that it does not wait indefinitely for an audio
stream.

Also disable audio on unknown codec or missing decoder on the
client-side, for the same reasons.
2023-02-28 12:35:08 +01:00
f2c65808fa Add record audio support
Make the recorder accept two input sources (video and audio), and mux
them into a single file.
2023-02-28 12:35:08 +01:00
93e86a5661 Rename video-specific variables in recorder
This paves the way to add audio-specific variables.
2023-02-28 12:35:08 +01:00
e614e19df4 Do not merge config audio packets
For video streams (at least H.264 and H.265), the config packet
containing SPS/PPS must be prepended to the next packet (the following
keyframe).

For audio streams (at least OPUS), they must not be merged.
2023-02-28 12:35:08 +01:00
e7c931139b Add an audio demuxer
Add a demuxer which will read the stream from the audio socket.
2023-02-28 12:35:08 +01:00
98ece15421 Give a name to demuxer instances
This will be useful in logs.
2023-02-28 12:35:08 +01:00
8050de011c Rename demuxer to video_demuxer
There will be another demuxer instance for audio.
2023-02-28 12:35:08 +01:00
714b01204a Extract OPUS extradata
For OPUS codec, FFmpeg expects the raw extradata, but MediaCodec wraps
it in some structure.

Fix the config packet to send only the raw extradata.
2023-02-28 12:35:08 +01:00
9e831005c4 Use a streamer to send the audio stream
Send each encoded audio packet using a streamer.
2023-02-28 12:35:08 +01:00
55eb874ed6 Encode recorded audio on the device
For now, the encoded packets are just logged into the console.
2023-02-28 12:35:08 +01:00
667882e9c4 Capture device audio
Create an AudioRecorder to capture the audio source REMOTE_SUBMIX.

For now, the captured packets are just logged into the console.

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-28 12:35:08 +01:00
0e62580570 Add a new socket for audio stream
When audio is enabled, open a new socket to send the audio stream from
the device to the client.

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-28 12:35:08 +01:00
ded19ca0f0 Add --no-audio option
Audio will be enabled by default (when supported). Add an option to
disable it.

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-28 12:35:08 +01:00
ad683461d6 Use FakeContext for Application instance
This will expose the correct package name and UID to the application
context.
2023-02-28 12:35:08 +01:00
504793b5c9 Use shell package name for workarounds
For consistency.
2023-02-28 12:35:08 +01:00
00e88acfaa Use ROOT_UID from FakeContext
Remove USER_ID from ServiceManager, and replace it by a constant in
FakeContext.

This is the same as android.os.Process.ROOT_UID, but this constant has
been introduced in API 29.
2023-02-28 12:35:08 +01:00
7c0ee70261 Use PACKAGE_NAME from FakeContext
Remove duplicated constant.
2023-02-28 12:35:08 +01:00
2a4eec702d Use AttributionSource from FakeContext
FakeContext already provides an AttributeSource instance.

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-02-28 12:35:08 +01:00
1e113feb59 Add a fake Android Context
Since scrcpy-server is not an Android application (it's a java
executable), it has no Context.

Some features will require a Context instance to get the package name
and the UID. Add a FakeContext for this purpose.

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-02-28 12:35:08 +01:00
08da34835c Improve error message for unknown encoder
The provided encoder name depends on the selected codec. Improve the
error message and the suggestions.
2023-02-28 12:35:08 +01:00
7b3a39bdc7 Rename "codec" variable to "mediaCodec"
This will allow to use "codec" for the Codec type.
2023-02-28 12:35:08 +01:00
5586335276 Make streamer independent of codec type
Rename VideoStreamer to Streamer, and extract a Codec interface which
will also support audio codecs.
2023-02-28 12:35:08 +01:00
8bb63cf14c Pass all args to ScreenEncoder constructor
There is no good reason to pass some of them in the constructor and some
others as parameters of the streamScreen() method.
2023-02-28 12:35:08 +01:00
5b01457364 Move screen encoder initialization
This prepares further refactors.
2023-02-28 12:35:08 +01:00
010da4df59 Write streamer header from ScreenEncoder
The screen encoder is responsible to write data to the video streamer.
2023-02-28 12:35:08 +01:00
de332e3e96 Use VideoStreamer directly from ScreenEncoder
The Callbacks interface notifies new packets. But in addition, the
screen encoder will need to write headers on start.

We could add a function onStart(), but for simplicity, just remove the
interface, which brings no value, and call the streamer directly.

Refs 87972e2022
2023-02-28 12:35:08 +01:00
e679e3a966 Simplify error handling on socket creation
On any error, all previously opened sockets must be closed.

Handle these errors in a single catch-block. Currently, there are only 2
sockets, but this will simplify even more with more sockets.

Note: this commit is better displayed with --ignore-space-change (-b).
2023-02-28 12:35:08 +01:00
97ae0a2d13 Reorder initialization
Initialize components in the pipeline order: demuxer first, decoder and
recorder second.
2023-02-28 12:35:08 +01:00
f1a4349834 Refactor recorder logic
Process the initial config packet (necessary to write the header)
separately.
2023-02-28 12:35:08 +01:00
a95bfe4f01 Move last packet recording
Write the last packet at the end.
2023-02-28 12:35:08 +01:00
84f1792c6f Add start() function for recorder
For consistency with the other components, do not start the internal
thread from an init() function.
2023-02-28 12:35:08 +01:00
317a5e93bb Open recording file from the recorder thread
The recorder opened the target file from the packet sink open()
callback, called by the demuxer. Only then the recorder thread was
started.

One golden rule for the recorder is to never block the demuxer for I/O,
because it would impact mirroring. This rule is respected on recording
packets, but not for the initial recorder opening.

Therefore, start the recorder thread from sc_recorder_init(), open the
file immediately from the recorder thread, then make it wait for the
stream to start (on packet sink open()).

Now that the recorder can report errors directly (rather than making the
demuxer call fail), it is possible to report file opening error even
before the packet sink is open.
2023-02-28 12:35:08 +01:00
6b669d2dba Inline packet_sink impl in recorder
Remove useless wrappers.
2023-02-28 12:35:08 +01:00
21dd946edc Initialize recorder fields from init()
The recorder has two initialization phases: one to initialize the
concrete recorder object, and one to open its packet_sink trait.

Initialize mutex and condvar as part of the object initialization.

If there were several packet_sink traits, the mutex and condvar would
still be initialized only once.
2023-02-28 12:35:08 +01:00
669cbc7457 Report recorder errors
Stop scrcpy on recorder errors.

It was previously indirectly stopped by the demuxer, which failed to
push packets to a recorder in error. Report it directly instead:
 - it avoids to wait for the next demuxer call;
 - it will allow to open the target file from a separate thread and stop
   immediately on any I/O error.
2023-02-28 12:35:08 +01:00
d3adda176b Move previous packet to a local variable
It is only used from run_recorder().
2023-02-28 12:35:08 +01:00
e7fa099be4 Move pts_origin to a local variable
It is only used from run_recorder().
2023-02-28 12:35:08 +01:00
7ec2c7e232 Change PTS origin type from uint64_t to int64_t
It is initialized from AVPacket.pts, which is an int64_t.
2023-02-28 12:35:08 +01:00
932e698cdd Fix --encoder documentation
Mention that it depends on the codec provided by --codec (which is not
necessarily H264 anymore).
2023-02-28 12:35:08 +01:00
ca5b962377 Do not print stacktraces when unnecessary
User-friendly error messages are printed on specific configuration
exceptions. In that case, do not print the stacktrace.

Also handle the user-friendly error message directly where the error
occurs, and print multiline messages in a single log call, to avoid
confusing interleaving.
2023-02-28 12:35:08 +01:00
9233f1990e Fix --no-clipboard-autosync bash completion
Fix typo.
2023-02-28 12:35:08 +01:00
17a486d763 Split server stop() and join()
For consistency with the other components, call stop() and join()
separately.

This allows to stop all components, then join them all.
2023-02-28 12:35:08 +01:00
65e9206b3f Print FFmpeg logs
FFmpeg logs are redirected to a specific SDL log category.

Initialize the log level for this category to print them as expected.
2023-02-28 12:35:08 +01:00
bf7c0f2c33 Move FFmpeg callback initialization
Configure FFmpeg log redirection on start from a log helper.
2023-02-28 12:35:08 +01:00
dc585f033e Silence lint warning about constant in API 29
MediaFormat.MIMETYPE_VIDEO_AV1 has been added in API 29, but it is not
a problem to inline the constant in older versions.
2023-02-28 12:35:08 +01:00
b67ea5173c Remove manifest package name
As reported by gradle:

> Setting the namespace via a source AndroidManifest.xml's package
> attribute is deprecated.
>
> Please instead set the namespace (or testNamespace) in the module's
> build.gradle file, as described here:
> https://developer.android.com/studio/build/configure-app-module#set-namespace
2023-02-28 12:35:08 +01:00
cf9718bee4 Upgrade gradle build tools to 7.4.0
Plugin version 7.4.0.
Gradle version 7.5.

Refs <https://developer.android.com/studio/releases/gradle-plugin#updating-gradle>
2023-02-28 12:35:08 +01:00
3c3c07db05 Initialize server->serial in all cases
Running scrcpy --tcpip on a device already connected via TCP/IP did not
initialize server->serial.

As a consequence, in debug mode, an assertion failed:

    scrcpy: ../app/src/server.c:770: run_server: Assertion
    `server->serial' failed.

In release mode, scrcpy failed with this error:

    adb: -s requires an argument
2023-02-28 12:34:34 +01:00
6b422e21bf Fix error message on icon loading failure 2023-02-27 20:51:54 +01:00
8e8b039a63 Do not use avformat network
Scrcpy does not use FFmpeg network features. Initialize network locally
instead (useful only for Windows).

The include block has been moved to fix the following warning:

    Please include winsock2.h before windows.h
2023-02-27 20:51:54 +01:00
0702be86d8 Accept Windows Sockets from version 1.1
Version 2.2 is probably not necessary (1.1 is the version required by
FFmpeg when network is enabled).

Refs <891ed24f77/libavformat/network.c (L63)>
Refs <https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-wsastartup>
2023-02-27 20:51:54 +01:00
0cea7fb24c Fix WSAStartup() error check on Windows 2023-02-27 20:51:35 +01:00
3d10fbd9b4 Fix --bit-rate option in bash completion script
The option is --bit-rate, not --bitrate.
2023-02-21 19:50:01 +01:00
3e3756a323 Add auto-completion for --codec option
Add missing command to bash and zsh completion scripts.
2023-02-21 19:48:28 +01:00
5d6bcc5966 Use enum for long options constants
This avoids to manually assign values.
2023-02-21 19:47:27 +01:00
5973d4cdd7 Initialize mouse_input_mode explicitly
The explicit initialization was missing. It had no consequences because
SC_MOUSE_INPUT_MODE_INJECT == 0.

Fixes #3749 <https://github.com/Genymobile/scrcpy/issues/3749>
2023-02-21 12:11:39 +01:00
0a151b96fe Accept muxing AV1 into MP4 container
MP4 supports AV1.

Refs d2dce51038
2023-02-20 20:49:56 +01:00
ebecbe6bc6 Fix inconsistent quotes
The encoder name started with a simple quote but ended with a
double quote. Use a single quote for both.
2023-02-19 16:18:19 +01:00
d5dff239c8 Suggest commands with an explicit '=' 2023-02-19 16:18:19 +01:00
5cf86ef7ff Move finally-block to fix deadlock on stop
DesktopConnection implements Closeable, so it is implicitly closed after
its try-with-resources block. Closing the DesktopConnection shutdowns
the sockets, so it is necessary in particular to wake up blocking read()
calls from the controller.

But the controller thread was joined before the DesktopConnection was
closed, causing a deadlock. To fix the problem, join the controller
thread only after the DesktopConnection is closed.

Refs 400a1c69b1
2023-02-19 15:59:05 +01:00
e02f30f895 Remove unnecessary error logs
When a call to a packet or frame sink fails, do not log the error on the
caller side: either the "failure" is expected (explicitly stopped) or it
must be logged by the packet or frame sink implementation.
2023-02-19 02:10:14 +01:00
25e2eb7d7c Document default video codec
Mention the default option value, like for other commands.
2023-02-18 19:10:25 +01:00
280a9afda8 Fix command-line help typo 2023-02-18 18:11:34 +01:00
e91618586c Prefix receiver by sc_
Like all other components in scrcpy.
2023-02-18 09:37:31 +01:00
680ddf64be Fix demuxer error message
Now that there are several possible codecs, do not hardcode H.264 in the
error message.

Refs 3e517cd40e
2023-02-18 09:31:06 +01:00
f4e7085c34 Log non-EPIPE I/O exceptions
On close, the client closes the socket. This wakes up socket blocking
calls on the server-side, by throwing an exception. Since this exception
is expected, it was not logged.

However, other IOExceptions might occur, which must not be ignored. For
that purpose, log only IOException when they are not caused by an EPIPE
error.
2023-02-17 08:33:16 +01:00
439a1fd4ed Rename 'uid' to 'scid'
A random identifier is generated to differentiate multiple running
scrcpy instances. Rename it from 'uid' to 'scid' (scrcpy id) not to
confuse it with Linux UID.

Fixes #3729 <https://github.com/Genymobile/scrcpy/issues/3729>
Refs 4315be1648
2023-02-11 09:58:40 +01:00
49eb326ce9 Extract packet merging
Config packets must be prepended to the next media packet. Extract the
logic to a new sc_packet_merger helper to simplify the demuxer code.
2023-02-11 09:44:33 +01:00
f03f32267e Remove unused parser
Since 1c02b58412, the parser is not used
anymore.
2023-02-10 23:18:11 +01:00
45b2e6db5c Log component stopped in finally clause
The message must be logged even when no exception occurs.
2023-02-10 19:06:17 +01:00
400a1c69b1 Join all threads before end of main
Some calls from separate threads may throw exceptions once the main()
method has returned.
2023-02-10 19:04:56 +01:00
730eb1086a Properly report demuxer errors
All demuxer errors were reported as "device disconnected", even if the
failure was not related to device socket read.
2023-02-10 18:55:43 +01:00
4f9e9c6619 Prefix UI events constants by SC_ 2023-02-10 18:55:43 +01:00
953edfd1df Split codec_id reading
Receive codec id and convert it to AVCodecID separately.

This will allow the caller to distinguish between EOS and unknown codec
id.
2023-02-10 18:55:43 +01:00
230b8274b9 Fix error return value
The function returns an enum AVCodecID, not a bool.
2023-02-10 18:55:43 +01:00
40866ddc10 Fix demuxer error message
The message applies to all packets, not only config packets.
2023-02-10 18:55:39 +01:00
bd56c0abf7 Remove unused codec context
The demuxer does not need any codec context.
2023-02-10 18:46:01 +01:00
6524e90c68 Remove unused constant
This line was committed by error.

Refs 3e517cd40e
2023-02-07 23:11:42 +01:00
f2dee20a20 Set power mode on all physical displays
Android 10 and above support multiple physical displays. Apply power
mode to all of them.

Fixes #3716 <https://github.com/Genymobile/scrcpy/issues/3716>
2023-02-06 11:07:14 +01:00
d2dce51038 Add support for AV1
Add option --codec=av1.

PR #3713 <https://github.com/Genymobile/scrcpy/pull/3713>
2023-02-06 11:00:49 +01:00
4342c5637d Add support for H265
Add option --codec=h265.

PR #3713 <https://github.com/Genymobile/scrcpy/pull/3713>
Fixes #3092 <https://github.com/Genymobile/scrcpy/issues/3092>
2023-02-06 11:00:49 +01:00
3e517cd40e Add option to select video codec
Introduce the selection mechanism. Alternative codecs will be added in
further commits.

PR #3713 <https://github.com/Genymobile/scrcpy/pull/3713>
2023-02-06 10:58:45 +01:00
f70f6cdd3e Simplify server info initialization
Use sc_read16be() to read 16-bit integer fields.
2023-02-03 12:31:28 +01:00
87972e2022 Extract video streaming to a separate class
ScreenEncoder handled both capture/encoding and sending over the
network.

Move the streaming part to a separate VideoStreamer.
2023-02-03 12:31:28 +01:00
3aac74e9e9 Move variable assignment
Computing eof flag is not necessary if rotation changed.
2023-02-03 12:31:28 +01:00
1c82c3923d Compute relative PTS on the client-side
The PTS received from MediaCodec are expressed relative to an arbitrary
clock origin. We consider the PTS of the first frame to be 0, and the
PTS of every other frame is relative to this first PTS (note that the
PTS is only used for recording, it is ignored for mirroring).

For simplicity, this relative PTS was computed on the server-side.

To prepare support for multiple stream (video and audio), send the
packet with its original PTS, and handle the PTS offset on the
client-side (by the recorder).

Since we can't know in advance which stream will produce the first
packet with the lowest PTS (a packet received later on one stream may
have a PTS lower than a packet received earlier on another stream),
computing the PTS on the server-side would require unnecessary waiting.
2023-02-03 12:31:28 +01:00
36d656e91f Improve workarounds call comments 2023-02-03 12:31:28 +01:00
bdbf1f4eb7 Move Workarounds call
Workarounds are not specific to the screen encoder.

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-02-03 12:31:28 +01:00
4177de5880 Do not expose controller threads
The way the controller executes its events asynchronously is an
implementation detail.
2023-02-03 12:31:28 +01:00
6a07e3d470 Fix manpage formatting
Only the option arguments must be underlined.
2023-02-03 12:31:28 +01:00
9b286ec8a7 Inject additional ACTION_BUTTON_* events for mouse
On mouse click events:
 - the first button pressed must first generate ACTION_DOWN;
 - all button pressed (including the first one) must generate
   ACTION_BUTTON_PRESS;
 - all button released (including the last one) must generate
   ACTION_BUTTON_RELEASE;
 - the last button released must in addition generate ACTION_UP.

Otherwise, Chrome does not work properly.

Fixes #3635 <https://github.com/Genymobile/scrcpy/issues/3635>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-01-30 20:57:54 +01:00
8c5c55f9e1 Fix mouse pointer state update
If the pointer is a mouse, the pointer is UP only when no buttons are
pressed (not when a button is released, because there might be other
buttons still pressed).

Refs #3635 <https://github.com/Genymobile/scrcpy/issues/3635>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-01-30 20:57:54 +01:00
0afef0c634 Forward action button to device
On click event, only the whole buttons state was passed to the device.
In addition, on ACTION_DOWN and ACTION_UP, pass the button associated to
the action.

Refs #3635 <https://github.com/Genymobile/scrcpy/issues/3635>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-01-30 20:57:54 +01:00
07806ba915 Retry on spurious error
MediaCodec may fail spuriously, typically when stopping an encoding and
starting a new one immediately (for example on device rotation).

In that case, retry a few times, in many cases it should work.

Refs #3693 <https://github.com/Genymobile/scrcpy/issues/3693>
2023-01-30 20:55:51 +01:00
a52053421a Extract retry handling
Move the code to downscale and retry on error out of the catch-block.

Refs 26b4104844
2023-01-29 14:49:36 +01:00
89 changed files with 4129 additions and 1107 deletions

View File

@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash
scrcpy --bit-rate=2M
scrcpy --video-bit-rate=2M
scrcpy -b 2M # short version
```
@ -252,20 +252,31 @@ This affects recording orientation.
The [window may also be rotated](#rotation) independently.
#### Encoder
#### Codec
Some devices have more than one encoder, and some of them may cause issues or
crash. It is possible to select a different encoder:
The video codec can be selected. The possible values are `h264` (default),
`h265` and `av1`:
```bash
scrcpy --encoder=OMX.qcom.video.encoder.avc
scrcpy --video-codec=h264 # default
scrcpy --video-codec=h265
scrcpy --video-codec=av1
```
To list the available encoders, you can pass an invalid encoder name; the
error will give the available encoders:
##### Encoder
Some devices have more than one encoder for a specific codec, and some of them
may cause issues or crash. It is possible to select a different encoder:
```bash
scrcpy --encoder=_
scrcpy --video-encoder=OMX.qcom.video.encoder.avc
```
To list the available encoders:
```bash
scrcpy --list-encoders
```
### Capture
@ -431,7 +442,7 @@ none found, try running `adb disconnect`, and then run those two commands again.
It may be useful to decrease the bit-rate and the resolution:
```bash
scrcpy --bit-rate=2M --max-size=800
scrcpy --video-bit-rate=2M --max-size=800
scrcpy -b2M -m800 # short version
```

View File

@ -2,28 +2,33 @@ _scrcpy() {
local cur prev words cword
local opts="
--always-on-top
-b --bit-rate=
--codec-options=
--audio-bit-rate=
--audio-codec=
--audio-codec-options=
--audio-encoder=
-b --video-bit-rate=
--crop=
-d --select-usb
--disable-screensaver
--display=
--display-buffer=
-e --select-tcpip
--encoder=
--force-adb-forward
--forward-all-clicks
-f --fullscreen
-K --hid-keyboard
-h --help
--legacy-paste
--list-displays
--list-encoders
--lock-video-orientation
--lock-video-orientation=
--max-fps=
-M --hid-mouse
-m --max-size=
--no-audio
--no-cleanup
--no-clipboard-on-error
--no-clipboard-autosync
--no-downsize-on-error
-n --no-control
-N --no-display
@ -53,6 +58,9 @@ _scrcpy() {
--v4l2-sink=
-V --verbosity=
-v --version
--video-codec=
--video-codec-options=
--video-encoder=
-w --stay-awake
--window-borderless
--window-title=
@ -64,6 +72,14 @@ _scrcpy() {
_init_completion -s || return
case "$prev" in
--video-codec)
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
return
;;
--audio-codec)
COMPREPLY=($(compgen -W 'opus aac' -- "$cur"))
return
;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return
@ -98,7 +114,7 @@ _scrcpy() {
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return
;;
-b|--bitrate \
-b|--video-bit-rate \
|--codec-options \
|--crop \
|--display \

View File

@ -9,25 +9,30 @@ local arguments
arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
{-b,--bit-rate=}'[Encode the video at the given bit-rate]'
'--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-codec=[Select the audio codec]:codec:(opus aac)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]'
'--display=[Specify the display id to mirror]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
{-e,--select-tcpip}'[Use TCP/IP device]'
'--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]'
{-f,--fullscreen}'[Start in fullscreen]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
{-h,--help}'[Print the help]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
'--max-fps=[Limit the frame rate of screen capture]'
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'--no-audio[Disable audio forwarding]'
'--no-cleanup[Disable device cleanup actions on exit]'
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
@ -58,6 +63,9 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
{-v,--version}'[Print the version of scrcpy]'
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]'
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
'--window-borderless[Disable window decorations \(display borderless window\)]'
'--window-title=[Set a custom window title]'

View File

@ -4,6 +4,7 @@ src = [
'src/adb/adb_device.c',
'src/adb/adb_parser.c',
'src/adb/adb_tunnel.c',
'src/audio_player.c',
'src/cli.c',
'src/clock.c',
'src/compat.c',
@ -21,6 +22,7 @@ src = [
'src/mouse_inject.c',
'src/opengl.c',
'src/options.c',
'src/packet_merger.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
@ -29,6 +31,8 @@ src = [
'src/version.c',
'src/video_buffer.c',
'src/util/acksync.c',
'src/util/average.c',
'src/util/bytebuf.c',
'src/util/file.c',
'src/util/intmap.c',
'src/util/intr.c',
@ -98,6 +102,7 @@ if not crossbuild_windows
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
]
@ -128,24 +133,19 @@ else
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
# ffmpeg versions are different for win32 and win64 builds
ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec')
ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat')
ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil')
ffmpeg = declare_dependency(
dependencies: [
cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir),
cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir),
cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir),
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root')
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include'
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
libusb = declare_dependency(
dependencies: [
@ -200,10 +200,6 @@ conf.set('PORTABLE', get_option('portable'))
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default video bitrate, in bits/second
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
@ -263,6 +259,10 @@ if get_option('buildtype') == 'debug'
['test_binary', [
'tests/test_binary.c',
]],
['test_bytebuf', [
'tests/test_bytebuf.c',
'src/util/bytebuf.c',
]],
['test_cbuf', [
'tests/test_cbuf.c',
]],

View File

@ -1,45 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=ffmpeg-win32-4.3.1
FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip
SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2
FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip
SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \
"$FILENAME_SHARED" "$SHA256SUM_SHARED"
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \
"$FILENAME_DEV" "$SHA256SUM_DEV"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared
unzip "../$FILENAME_SHARED" \
"$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \
"$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \
"$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \
"$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \
"$ZIP_PREFIX_SHARED"/bin/swscale-5.dll
ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev
unzip "../$FILENAME_DEV" \
"$ZIP_PREFIX_DEV/include/*"
mv "$ZIP_PREFIX_SHARED"/* .
mv "$ZIP_PREFIX_DEV"/* .
rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV"

View File

@ -1,36 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=5.1.2
DEP_DIR=ffmpeg-win64-$VERSION
FILENAME=ffmpeg-$VERSION-full_build-shared.7z
SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared
7z x "../$FILENAME" \
"$ZIP_PREFIX"/bin/avutil-57.dll \
"$ZIP_PREFIX"/bin/avcodec-59.dll \
"$ZIP_PREFIX"/bin/avformat-59.dll \
"$ZIP_PREFIX"/bin/swresample-4.dll \
"$ZIP_PREFIX"/bin/swscale-6.dll \
"$ZIP_PREFIX"/include
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"

View File

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

View File

@ -22,13 +22,12 @@ get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
# include/ is the same in all folders of the archive
7z x "../$FILENAME" \
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
libusb-1.0.26-binaries/libusb-MinGW-x64/include/
mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32
mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64
mv libusb-1.0.26-binaries/libusb-MinGW-x64/include .
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
rm -rf libusb-1.0.26-binaries

View File

@ -20,20 +20,28 @@ provides display and control of Android devices connected on USB (or over TCP/IP
Make scrcpy window always on top (above other windows).
.TP
.BI "\-b, \-\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
.BI "\-\-audio\-bit\-rate " value
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8000000.
Default is 196K (196000).
.TP
.BI "\-\-codec\-options " key[:type]=value[,...]
Set a list of comma-separated key:type=value options for the device encoder.
.BI "\-\-audio\-codec " name
Select an audio codec (opus or aac).
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
Default is opus.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
.TP
.BI "\-b, \-\-video\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8M (8000000).
.TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
@ -55,10 +63,9 @@ Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display " id
Specify the display id to mirror.
Specify the device display id to mirror.
The list of possible display ids can be listed by "adb shell dumpsys display"
(search "mDisplayId=" in the output).
The available display ids can be listed by \-\-list\-displays.
Default is 0.
@ -74,10 +81,6 @@ Use TCP/IP device (if there is exactly one, like adb -e).
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
.TP
.BI "\-\-encoder " name
Use a specific MediaCodec encoder (must be a H.264 encoder).
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
@ -117,7 +120,15 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.BI "\-\-lock\-video\-orientation[=value]
.B \-\-list\-encoders
List video and audio encoders available on the device.
.TP
.B \-\-list\-displays
List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
Default is "unlocked".
@ -199,7 +210,7 @@ It may only work over USB.
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
.TP
.BI "\-p, \-\-port " port[:port]
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
Set the TCP port (range) used by the client to listen.
Default is 27183:27199.
@ -260,7 +271,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.BI "\-\-shortcut\-mod " key[+...]][,...]
.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 ','.
@ -270,7 +281,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-tcpip[=ip[:port]]
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
@ -323,6 +334,28 @@ Default is "info" for release builds, "debug" for debug builds.
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI "\-\-video\-codec " name
Select a video codec (h264, h265 or av1).
Default is h264.
.TP
.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
Set a list of comma-separated key:type=value options for the device video encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
.TP
.BI "\-\-video\-encoder " name
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running, when the device is plugged in.

304
app/src/audio_player.c Normal file
View File

@ -0,0 +1,304 @@
#include "audio_player.h"
#include <libavutil/opt.h>
#include "util/log.h"
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
/** Downcast frame_sink to sc_audio_player */
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
#define SC_SDL_SAMPLE_FMT AUDIO_F32
#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 480 // 10ms at 48000Hz
// The target number of buffered samples between the producer and the consumer.
// This value is directly use for compensation.
#define SC_TARGET_BUFFERED_SAMPLES (3 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES)
// If the consumer is too late, skip samples to keep at most this value
#define SC_BUFFERED_SAMPLES_THRESHOLD 2400 // 50ms at 48000Hz
// Use a ring-buffer of 1 second (at 48000Hz) between the producer and the
// consumer. It too big, but it guarantees that the producer and the consumer
// will be able to access it in parallel without locking.
#define SC_BYTEBUF_SIZE_IN_SAMPLES 48000
void
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
// the bytebuf is protected
assert(len_int > 0);
size_t len = len_int;
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] SDL callback requests %" SC_PRIsizet " samples",
len / (ap->nb_channels * ap->out_bytes_per_sample));
#endif
size_t read = sc_bytebuf_read_remaining(&ap->buf);
size_t max_buffered_bytes = SC_BUFFERED_SAMPLES_THRESHOLD
* ap->nb_channels * ap->out_bytes_per_sample;
if (read > max_buffered_bytes + len) {
size_t skip = read - (max_buffered_bytes + len);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffered samples threshold exceeded: %" SC_PRIsizet
" bytes, skipping %" SC_PRIsizet " bytes", read, skip);
#endif
// After this callback, exactly max_buffered_bytes will remain
sc_bytebuf_skip(&ap->buf, skip);
read = max_buffered_bytes + len;
}
// Number of buffered samples (may be negative on underflow)
float buffered_samples = ((float) read - len_int)
/ (ap->nb_channels * ap->out_bytes_per_sample);
sc_average_push(&ap->avg_buffered_samples, buffered_samples);
if (read) {
if (read > len) {
read = len;
}
sc_bytebuf_read(&ap->buf, stream, read);
}
if (read < len) {
// Insert silence
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffer underflow, inserting silence: %" SC_PRIsizet
" bytes", len - read);
#endif
memset(stream + read, 0, len - read);
}
}
static size_t
sc_audio_player_get_buf_size(struct sc_audio_player *ap, size_t samples) {
assert(ap->nb_channels);
assert(ap->out_bytes_per_sample);
return samples * ap->nb_channels * ap->out_bytes_per_sample;
}
static uint8_t *
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
size_t min_buf_size = sc_audio_player_get_buf_size(ap, min_samples);
if (min_buf_size < ap->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ap->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ap->swr_buf = buf;
ap->swr_buf_alloc_size = new_size;
}
return ap->swr_buf;
}
static bool
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
assert(ctx->ch_layout.nb_channels > 0);
unsigned nb_channels = ctx->ch_layout.nb_channels;
#else
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
assert(tmp > 0);
unsigned nb_channels = tmp;
#endif
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels,
.samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
.callback = sc_audio_player_sdl_callback,
.userdata = ap,
};
SDL_AudioSpec obtained;
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (!ap->device) {
LOGE("Could not open audio device: %s", SDL_GetError());
return false;
}
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
goto error_close_audio_device;
}
ap->swr_ctx = swr_ctx;
assert(ctx->sample_rate > 0);
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
assert(out_bytes_per_sample > 0);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
ap->sample_rate = ctx->sample_rate;
ap->nb_channels = nb_channels;
ap->out_bytes_per_sample = out_bytes_per_sample;
size_t bytebuf_size =
sc_audio_player_get_buf_size(ap, SC_BYTEBUF_SIZE_IN_SAMPLES);
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
if (!ok) {
goto error_free_swr_ctx;
}
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
size_t initial_swr_buf_size = sc_audio_player_get_buf_size(ap, 4096);
ap->swr_buf = malloc(initial_swr_buf_size);
if (!ap->swr_buf) {
LOG_OOM();
goto error_destroy_bytebuf;
}
ap->swr_buf_alloc_size = initial_swr_buf_size;
sc_average_init(&ap->avg_buffered_samples, 32);
ap->samples_since_resync = 0;
SDL_PauseAudioDevice(ap->device, 0);
return true;
error_destroy_bytebuf:
sc_bytebuf_destroy(&ap->buf);
error_free_swr_ctx:
swr_free(&ap->swr_ctx);
error_close_audio_device:
SDL_CloseAudioDevice(ap->device);
return false;
}
static void
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_audio_player *ap = DOWNCAST(sink);
assert(ap->device);
SDL_PauseAudioDevice(ap->device, 1);
SDL_CloseAudioDevice(ap->device);
free(ap->swr_buf);
sc_bytebuf_destroy(&ap->buf);
swr_free(&ap->swr_ctx);
}
static bool
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_audio_player *ap = DOWNCAST(sink);
SwrContext *swr_ctx = ap->swr_ctx;
int64_t delay = swr_get_delay(swr_ctx, ap->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same
int dst_nb_samples = delay + frame->nb_samples;
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, frame->nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
size_t samples_written = ret;
size_t swr_buf_size = sc_audio_player_get_buf_size(ap, samples_written);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGI("[Audio] %" SC_PRIsizet " samples written to buffer", samples_written);
#endif
// It should almost always be possible to write without lock
bool can_write_without_lock = swr_buf_size <= ap->safe_empty_buffer;
if (can_write_without_lock) {
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
}
SDL_LockAudioDevice(ap->device);
if (can_write_without_lock) {
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
} else {
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
}
// The next time, it will remain at least the current empty space
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
// Read the value written by the SDL thread under lock
float avg;
bool has_avg = sc_average_get(&ap->avg_buffered_samples, &avg);
SDL_UnlockAudioDevice(ap->device);
if (has_avg) {
ap->samples_since_resync += samples_written;
if (ap->samples_since_resync >= ap->sample_rate) {
// Resync every second
ap->samples_since_resync = 0;
int diff = SC_TARGET_BUFFERED_SAMPLES - avg;
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGI("[Audio] Average buffered samples = %f, compensation %d",
avg, diff);
#endif
// Compensate the diff over 3 seconds (but will be recomputed after
// 1 second)
int ret = swr_set_compensation(swr_ctx, diff, 3 * ap->sample_rate);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
}
}
}
return true;
}
void
sc_audio_player_init(struct sc_audio_player *ap) {
static const struct sc_frame_sink_ops ops = {
.open = sc_audio_player_frame_sink_open,
.close = sc_audio_player_frame_sink_close,
.push = sc_audio_player_frame_sink_push,
};
ap->frame_sink.ops = &ops;
}

54
app/src/audio_player.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef SC_AUDIO_PLAYER_H
#define SC_AUDIO_PLAYER_H
#include "common.h"
#include <stdbool.h>
#include "trait/frame_sink.h"
#include <util/average.h>
#include <util/bytebuf.h>
#include <util/thread.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
struct sc_audio_player {
struct sc_frame_sink frame_sink;
SDL_AudioDeviceID device;
// protected by SDL_AudioDeviceLock()
struct sc_bytebuf buf;
// Number of bytes which could be written without locking
size_t safe_empty_buffer;
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
unsigned sample_rate;
// The number of channels is the same for input and output
unsigned nb_channels;
unsigned out_bytes_per_sample;
// Target buffer for resampling
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow)
struct sc_average avg_buffered_samples;
unsigned samples_since_resync;
const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata;
};
struct sc_audio_player_callbacks {
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
};
void
sc_audio_player_init(struct sc_audio_player *ap);
#endif

View File

@ -17,46 +17,59 @@
#define STR_IMPL_(x) #x
#define STR(x) STR_IMPL_(x)
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014
#define OPT_ROTATION 1015
#define OPT_RENDER_DRIVER 1016
#define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018
#define OPT_FORCE_ADB_FORWARD 1019
#define OPT_DISABLE_SCREENSAVER 1020
#define OPT_SHORTCUT_MOD 1021
#define OPT_NO_KEY_REPEAT 1022
#define OPT_FORWARD_ALL_CLICKS 1023
#define OPT_LEGACY_PASTE 1024
#define OPT_ENCODER_NAME 1025
#define OPT_POWER_OFF_ON_CLOSE 1026
#define OPT_V4L2_SINK 1027
#define OPT_DISPLAY_BUFFER 1028
#define OPT_V4L2_BUFFER 1029
#define OPT_TUNNEL_HOST 1030
#define OPT_TUNNEL_PORT 1031
#define OPT_NO_CLIPBOARD_AUTOSYNC 1032
#define OPT_TCPIP 1033
#define OPT_RAW_KEY_EVENTS 1034
#define OPT_NO_DOWNSIZE_ON_ERROR 1035
#define OPT_OTG 1036
#define OPT_NO_CLEANUP 1037
#define OPT_PRINT_FPS 1038
#define OPT_NO_POWER_ON 1039
enum {
OPT_RENDER_EXPIRED_FRAMES = 1000,
OPT_WINDOW_TITLE,
OPT_PUSH_TARGET,
OPT_ALWAYS_ON_TOP,
OPT_CROP,
OPT_RECORD_FORMAT,
OPT_PREFER_TEXT,
OPT_WINDOW_X,
OPT_WINDOW_Y,
OPT_WINDOW_WIDTH,
OPT_WINDOW_HEIGHT,
OPT_WINDOW_BORDERLESS,
OPT_MAX_FPS,
OPT_LOCK_VIDEO_ORIENTATION,
OPT_DISPLAY_ID,
OPT_ROTATION,
OPT_RENDER_DRIVER,
OPT_NO_MIPMAPS,
OPT_CODEC_OPTIONS,
OPT_VIDEO_CODEC_OPTIONS,
OPT_FORCE_ADB_FORWARD,
OPT_DISABLE_SCREENSAVER,
OPT_SHORTCUT_MOD,
OPT_NO_KEY_REPEAT,
OPT_FORWARD_ALL_CLICKS,
OPT_LEGACY_PASTE,
OPT_ENCODER,
OPT_VIDEO_ENCODER,
OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER,
OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT,
OPT_NO_CLIPBOARD_AUTOSYNC,
OPT_TCPIP,
OPT_RAW_KEY_EVENTS,
OPT_NO_DOWNSIZE_ON_ERROR,
OPT_OTG,
OPT_NO_CLEANUP,
OPT_PRINT_FPS,
OPT_NO_POWER_ON,
OPT_CODEC,
OPT_VIDEO_CODEC,
OPT_NO_AUDIO,
OPT_AUDIO_BIT_RATE,
OPT_AUDIO_CODEC,
OPT_AUDIO_CODEC_OPTIONS,
OPT_AUDIO_ENCODER,
OPT_LIST_ENCODERS,
OPT_LIST_DISPLAYS,
};
struct sc_option {
char shortopt;
@ -98,25 +111,63 @@ static const struct sc_option options[] = {
.text = "Make scrcpy window always on top (above other windows).",
},
{
.shortopt = 'b',
.longopt = "bit-rate",
.longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate",
.argdesc = "value",
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is " STR(DEFAULT_BIT_RATE) ".",
"Default is 196K (196000).",
},
{
.longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options",
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
.argdesc = "name",
.text = "Select an audio codec (opus or aac).\n"
"Default is opus.",
},
{
.longopt_id = OPT_AUDIO_CODEC_OPTIONS,
.longopt = "audio-codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device encoder.\n"
"device audio encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec audio encoder (depending on the "
"codec provided by --audio-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.shortopt = 'b',
.longopt = "video-bit-rate",
.argdesc = "value",
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is 8M (8000000).",
},
{
// Not really deprecated (--codec has never been released), but without
// declaring an explicit --codec option, getopt_long() partial matching
// behavior would consider --codec to be equivalent to --codec-options,
// which would be confusing.
.longopt_id = OPT_CODEC,
.longopt = "codec",
.argdesc = "value",
},
{
// deprecated
.longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options",
.argdesc = "key[:type]=value[,...]",
},
{
.longopt_id = OPT_CROP,
.longopt = "crop",
@ -141,10 +192,9 @@ static const struct sc_option options[] = {
.longopt_id = OPT_DISPLAY_ID,
.longopt = "display",
.argdesc = "id",
.text = "Specify the display id to mirror.\n"
"The list of possible display ids can be listed by:\n"
" adb shell dumpsys display\n"
"(search \"mDisplayId=\" in the output)\n"
.text = "Specify the device display id to mirror.\n"
"The available display ids can be listed by:\n"
" scrcpy --list-displays\n"
"Default is 0.",
},
{
@ -162,10 +212,10 @@ static const struct sc_option options[] = {
"Also see -d (--select-usb).",
},
{
.longopt_id = OPT_ENCODER_NAME,
// deprecated
.longopt_id = OPT_ENCODER,
.longopt = "encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
},
{
.longopt_id = OPT_FORCE_ADB_FORWARD,
@ -215,6 +265,16 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.",
},
{
.longopt_id = OPT_LIST_DISPLAYS,
.longopt = "list-displays",
.text = "List device displays.",
},
{
.longopt_id = OPT_LIST_ENCODERS,
.longopt = "list-encoders",
.text = "List video and audio encoders available on the device.",
},
{
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation",
@ -256,6 +316,11 @@ static const struct sc_option options[] = {
"is preserved.\n"
"Default is 0 (unlimited).",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{
.longopt_id = OPT_NO_CLEANUP,
.longopt = "no-cleanup",
@ -502,6 +567,33 @@ static const struct sc_option options[] = {
.longopt = "version",
.text = "Print the version of scrcpy.",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
.argdesc = "name",
.text = "Select a video codec (h264, h265 or av1).\n"
"Default is h264.",
},
{
.longopt_id = OPT_VIDEO_CODEC_OPTIONS,
.longopt = "video-codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device video encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_VIDEO_ENCODER,
.longopt = "video-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec video encoder (depending on the "
"codec provided by --video-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.shortopt = 'w',
.longopt = "stay-awake",
@ -1377,6 +1469,38 @@ guess_record_format(const char *filename) {
return 0;
}
static bool
parse_video_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "h264")) {
*codec = SC_CODEC_H264;
return true;
}
if (!strcmp(optarg, "h265")) {
*codec = SC_CODEC_H265;
return true;
}
if (!strcmp(optarg, "av1")) {
*codec = SC_CODEC_AV1;
return true;
}
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
return false;
}
static bool
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "opus")) {
*codec = SC_CODEC_OPUS;
return true;
}
if (!strcmp(optarg, "aac")) {
*codec = SC_CODEC_AAC;
return true;
}
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
return false;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@ -1388,7 +1512,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
switch (c) {
case 'b':
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
return false;
}
break;
case OPT_AUDIO_BIT_RATE:
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
return false;
}
break;
@ -1561,10 +1690,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
opts->codec_options = optarg;
LOGW("--codec-options is deprecated, use --video-codec-options "
"instead.");
// fall through
case OPT_VIDEO_CODEC_OPTIONS:
opts->video_codec_options = optarg;
break;
case OPT_ENCODER_NAME:
opts->encoder_name = optarg;
case OPT_AUDIO_CODEC_OPTIONS:
opts->audio_codec_options = optarg;
break;
case OPT_ENCODER:
LOGW("--encoder is deprecated, use --video-encoder instead.");
// fall through
case OPT_VIDEO_ENCODER:
opts->video_encoder = optarg;
break;
case OPT_AUDIO_ENCODER:
opts->audio_encoder = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
@ -1601,6 +1743,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_DOWNSIZE_ON_ERROR:
opts->downsize_on_error = false;
break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
case OPT_NO_CLEANUP:
opts->cleanup = false;
break;
@ -1610,6 +1755,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_PRINT_FPS:
opts->start_fps_counter = true;
break;
case OPT_CODEC:
LOGW("--codec is deprecated, use --video-codec instead.");
// fall through
case OPT_VIDEO_CODEC:
if (!parse_video_codec(optarg, &opts->video_codec)) {
return false;
}
break;
case OPT_AUDIO_CODEC:
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
return false;
}
break;
case OPT_OTG:
#ifdef HAVE_USB
opts->otg = true;
@ -1637,6 +1795,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
return false;
#endif
case OPT_LIST_ENCODERS:
opts->list_encoders = true;
break;
case OPT_LIST_DISPLAYS:
opts->list_displays = true;
break;
default:
// getopt prints the error message on stderr
return false;

View File

@ -37,6 +37,13 @@
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
#endif
// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API
// has been replaced by chlayout in FFmpeg commit
// f423497b455da06c1337846902c770028760e094.
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100)
# define SCRCPY_LAVU_HAS_CHLAYOUT
#endif
#if SDL_VERSION_ATLEAST(2, 0, 6)
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS

View File

@ -117,8 +117,9 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
uint16_t pressure =
sc_float_to_u16fp(msg->inject_touch_event.pressure);
sc_write16be(&buf[22], pressure);
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28;
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
return 32;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
int16_t hscroll =
@ -179,22 +180,25 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
if (pointer_name) {
// string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%f buttons=%06lx",
" pressure=%f action_button=%06lx buttons=%06lx",
pointer_name,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
} else {
// numeric pointer id
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%f buttons=%06lx",
PRIi32 " pressure=%f action_button=%06lx"
" buttons=%06lx",
id,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
}
break;

View File

@ -65,6 +65,7 @@ struct sc_control_msg {
} inject_text;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons action_button;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
struct sc_position position;

View File

@ -9,20 +9,20 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
struct sc_acksync *acksync) {
cbuf_init(&controller->queue);
bool ok = receiver_init(&controller->receiver, control_socket, acksync);
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
if (!ok) {
return false;
}
ok = sc_mutex_init(&controller->mutex);
if (!ok) {
receiver_destroy(&controller->receiver);
sc_receiver_destroy(&controller->receiver);
return false;
}
ok = sc_cond_init(&controller->msg_cond);
if (!ok) {
receiver_destroy(&controller->receiver);
sc_receiver_destroy(&controller->receiver);
sc_mutex_destroy(&controller->mutex);
return false;
}
@ -43,7 +43,7 @@ sc_controller_destroy(struct sc_controller *controller) {
sc_control_msg_destroy(&msg);
}
receiver_destroy(&controller->receiver);
sc_receiver_destroy(&controller->receiver);
}
bool
@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
return false;
}
if (!receiver_start(&controller->receiver)) {
if (!sc_receiver_start(&controller->receiver)) {
sc_controller_stop(controller);
sc_thread_join(&controller->thread, NULL);
return false;
@ -137,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) {
void
sc_controller_join(struct sc_controller *controller) {
sc_thread_join(&controller->thread, NULL);
receiver_join(&controller->receiver);
sc_receiver_join(&controller->receiver);
}

View File

@ -21,7 +21,7 @@ struct sc_controller {
sc_cond msg_cond;
bool stopped;
struct sc_control_msg_queue queue;
struct receiver receiver;
struct sc_receiver receiver;
};
bool

View File

@ -2,6 +2,7 @@
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include "events.h"
#include "video_buffer.h"
@ -25,11 +26,10 @@ sc_decoder_close_sinks(struct sc_decoder *decoder) {
}
static bool
sc_decoder_open_sinks(struct sc_decoder *decoder) {
sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->open(sink)) {
LOGE("Could not open frame sink %d", i);
if (!sink->ops->open(sink, ctx)) {
sc_decoder_close_first_sinks(decoder, i);
return false;
}
@ -48,8 +48,23 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
if (codec->type == AVMEDIA_TYPE_VIDEO) {
// Hardcoded video properties
decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
// Hardcoded audio properties
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
decoder->codec_ctx->ch_layout =
(AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
#else
decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
decoder->codec_ctx->channels = 2;
#endif
decoder->codec_ctx->sample_rate = 48000;
}
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
LOGE("Could not open codec");
LOGE("Decoder '%s': could not open codec", decoder->name);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
@ -62,8 +77,7 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
return false;
}
if (!sc_decoder_open_sinks(decoder)) {
LOGE("Could not open decoder sinks");
if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) {
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
@ -86,7 +100,6 @@ push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->push(sink, frame)) {
LOGE("Could not send frame to sink %d", i);
return false;
}
}
@ -104,7 +117,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Could not send video packet: %d", ret);
LOGE("Decoder '%s': could not send video packet: %d",
decoder->name, ret);
return false;
}
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
@ -117,7 +131,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
av_frame_unref(decoder->frame);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
LOGE("Decoder '%s', could not receive video frame: %d",
decoder->name, ret);
return false;
}
return true;
@ -143,7 +158,8 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
}
void
sc_decoder_init(struct sc_decoder *decoder) {
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
decoder->name = name; // statically allocated
decoder->sink_count = 0;
static const struct sc_packet_sink_ops ops = {

View File

@ -14,6 +14,8 @@
struct sc_decoder {
struct sc_packet_sink packet_sink; // packet sink trait
const char *name; // must be statically allocated (e.g. a string literal)
struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS];
unsigned sink_count;
@ -21,8 +23,9 @@ struct sc_decoder {
AVFrame *frame;
};
// The name must be statically allocated (e.g. a string literal)
void
sc_decoder_init(struct sc_decoder *decoder);
sc_decoder_init(struct sc_decoder *decoder, const char *name);
void
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);

View File

@ -6,6 +6,7 @@
#include "decoder.h"
#include "events.h"
#include "packet_merger.h"
#include "recorder.h"
#include "util/binary.h"
#include "util/log.h"
@ -17,6 +18,42 @@
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
static enum AVCodecID
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
switch (codec_id) {
case SC_CODEC_ID_H264:
return AV_CODEC_ID_H264;
case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1:
return AV_CODEC_ID_AV1;
case SC_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC:
return AV_CODEC_ID_AAC;
default:
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
return AV_CODEC_ID_NONE;
}
}
static bool
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
uint8_t data[4];
ssize_t r = net_recv_all(demuxer->socket, data, 4);
if (r < 4) {
return false;
}
*codec_id = sc_read32be(data);
return true;
}
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
@ -80,7 +117,6 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (!sink->ops->push(sink, packet)) {
LOGE("Could not send config packet to sink %d", i);
return false;
}
}
@ -90,50 +126,9 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
static bool
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immediately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (demuxer->pending || is_config) {
if (demuxer->pending) {
size_t offset = demuxer->pending->size;
if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM();
return false;
}
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
} else {
demuxer->pending = av_packet_alloc();
if (!demuxer->pending) {
LOG_OOM();
return false;
}
if (av_packet_ref(demuxer->pending, packet)) {
LOG_OOM();
av_packet_free(&demuxer->pending);
return false;
}
}
if (!is_config) {
// prepare the concat packet to send to the decoder
demuxer->pending->pts = packet->pts;
demuxer->pending->dts = packet->dts;
demuxer->pending->flags = packet->flags;
packet = demuxer->pending;
}
}
bool ok = push_packet_to_sinks(demuxer, packet);
if (!is_config && demuxer->pending) {
// the pending packet must be discarded (consumed or error)
av_packet_free(&demuxer->pending);
}
if (!ok) {
LOGE("Could not process packet");
LOGE("Demuxer '%s': could not process packet", demuxer->name);
return false;
}
@ -158,7 +153,6 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (!sink->ops->open(sink, codec)) {
LOGE("Could not open packet sink %d", i);
sc_demuxer_close_first_sinks(demuxer, i);
return false;
}
@ -167,50 +161,99 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
return true;
}
static void
sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (sink->ops->disable) {
sink->ops->disable(sink);
}
}
}
static int
run_demuxer(void *data) {
struct sc_demuxer *demuxer = data;
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
// Flag to report end-of-stream (i.e. device disconnected)
bool eos = false;
uint32_t raw_codec_id;
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
if (!ok) {
LOGE("Demuxer '%s': stream disabled due to connection error",
demuxer->name);
eos = true;
goto end;
}
demuxer->codec_ctx = avcodec_alloc_context3(codec);
if (!demuxer->codec_ctx) {
LOG_OOM();
if (raw_codec_id == 0) {
LOGW("Demuxer '%s': stream explicitly disabled by the device",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
eos = true;
goto end;
}
if (raw_codec_id == 1) {
LOGE("Demuxer '%s': stream configuration error on the device",
demuxer->name);
goto end;
}
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
if (codec_id == AV_CODEC_ID_NONE) {
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
goto end;
}
const AVCodec *codec = avcodec_find_decoder(codec_id);
if (!codec) {
LOGE("Demuxer '%s': stream disabled due to missing decoder",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
goto end;
}
if (!sc_demuxer_open_sinks(demuxer, codec)) {
LOGE("Could not open demuxer sinks");
goto finally_free_codec_ctx;
goto end;
}
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
if (!demuxer->parser) {
LOGE("Could not initialize parser");
goto finally_close_sinks;
}
// Config packets must be merged with the next non-config packet only for
// video streams
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
struct sc_packet_merger merger;
if (must_merge_config_packet) {
sc_packet_merger_init(&merger);
}
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOG_OOM();
goto finally_close_parser;
goto finally_close_sinks;
}
for (;;) {
bool ok = sc_demuxer_recv_packet(demuxer, packet);
if (!ok) {
// end of stream
eos = true;
break;
}
if (must_merge_config_packet) {
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (!ok) {
av_packet_unref(packet);
break;
}
}
ok = sc_demuxer_push_packet(demuxer, packet);
av_packet_unref(packet);
if (!ok) {
@ -219,33 +262,31 @@ run_demuxer(void *data) {
}
}
LOGD("End of frames");
LOGD("Demuxer '%s': end of frames", demuxer->name);
if (demuxer->pending) {
av_packet_free(&demuxer->pending);
if (must_merge_config_packet) {
sc_packet_merger_destroy(&merger);
}
av_packet_free(&packet);
finally_close_parser:
av_parser_close(demuxer->parser);
finally_close_sinks:
sc_demuxer_close_sinks(demuxer);
finally_free_codec_ctx:
avcodec_free_context(&demuxer->codec_ctx);
end:
demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
return 0;
}
void
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
assert(socket != SC_SOCKET_NONE);
demuxer->name = name; // statically allocated
demuxer->socket = socket;
demuxer->pending = NULL;
demuxer->sink_count = 0;
assert(cbs && cbs->on_eos);
assert(cbs && cbs->on_ended);
demuxer->cbs = cbs;
demuxer->cbs_userdata = cbs_userdata;
@ -261,12 +302,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
bool
sc_demuxer_start(struct sc_demuxer *demuxer) {
LOGD("Starting demuxer thread");
LOGD("Demuxer '%s': starting thread", demuxer->name);
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
demuxer);
if (!ok) {
LOGE("Could not start demuxer thread");
LOGE("Demuxer '%s': could not start thread", demuxer->name);
return false;
}
return true;

View File

@ -15,28 +15,25 @@
#define SC_DEMUXER_MAX_SINKS 2
struct sc_demuxer {
const char *name; // must be statically allocated (e.g. a string literal)
sc_socket socket;
sc_thread thread;
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
unsigned sink_count;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
AVPacket *pending;
const struct sc_demuxer_callbacks *cbs;
void *cbs_userdata;
};
struct sc_demuxer_callbacks {
void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
};
// The name must be statically allocated (e.g. a string literal)
void
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
void

View File

@ -1,5 +1,7 @@
#define EVENT_NEW_FRAME SDL_USEREVENT
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
#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)

View File

@ -69,7 +69,7 @@ decode_image(const char *path) {
}
if (avformat_open_input(&ctx, path, NULL, NULL) < 0) {
LOGE("Could not open image codec: %s", path);
LOGE("Could not open icon image: %s", path);
goto free_ctx;
}

View File

@ -339,6 +339,7 @@ simulate_virtual_finger(struct sc_input_manager *im,
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: 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;
if (!sc_controller_push_msg(im->controller, &msg)) {

View File

@ -4,10 +4,6 @@
#include <stdbool.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#ifdef _WIN32
#include <windows.h>
#include "util/str.h"
#endif
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
@ -19,8 +15,14 @@
#include "scrcpy.h"
#include "usb/scrcpy_otg.h"
#include "util/log.h"
#include "util/net.h"
#include "version.h"
#ifdef _WIN32
#include <windows.h>
#include "util/str.h"
#endif
int
main_scrcpy(int argc, char *argv[]) {
#ifdef _WIN32
@ -69,10 +71,12 @@ main_scrcpy(int argc, char *argv[]) {
}
#endif
if (avformat_network_init()) {
if (!net_init()) {
return SCRCPY_EXIT_FAILURE;
}
sc_log_configure();
#ifdef HAVE_USB
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
: scrcpy(&args.opts);
@ -80,8 +84,6 @@ main_scrcpy(int argc, char *argv[]) {
enum scrcpy_exit_code ret = scrcpy(&args.opts);
#endif
avformat_network_deinit(); // ignore failure
return ret;
}

View File

@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.pointer_id = event->pointer_id,
.position = event->position,
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
.action_button = convert_mouse_buttons(event->button),
.buttons = convert_mouse_buttons(event->buttons_state),
},
};

View File

@ -7,14 +7,19 @@ const struct scrcpy_options scrcpy_options_default = {
.window_title = NULL,
.push_target = NULL,
.render_driver = NULL,
.codec_options = NULL,
.encoder_name = NULL,
.video_codec_options = NULL,
.audio_codec_options = NULL,
.video_encoder = NULL,
.audio_encoder = NULL,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
#endif
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
.port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
.last = DEFAULT_LOCAL_PORT_RANGE_LAST,
@ -26,7 +31,8 @@ const struct scrcpy_options scrcpy_options_default = {
.count = 2,
},
.max_size = 0,
.bit_rate = DEFAULT_BIT_RATE,
.video_bit_rate = 0,
.audio_bit_rate = 0,
.max_fps = 0,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.rotation = 0,
@ -65,4 +71,7 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.audio = true,
.list_encoders = false,
.list_displays = false,
};

View File

@ -23,6 +23,14 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKV,
};
enum sc_codec {
SC_CODEC_H264,
SC_CODEC_H265,
SC_CODEC_AV1,
SC_CODEC_OPUS,
SC_CODEC_AAC,
};
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
@ -87,12 +95,16 @@ struct scrcpy_options {
const char *window_title;
const char *push_target;
const char *render_driver;
const char *codec_options;
const char *encoder_name;
const char *video_codec_options;
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
#ifdef HAVE_V4L2
const char *v4l2_device;
#endif
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;
@ -101,7 +113,8 @@ struct scrcpy_options {
uint16_t tunnel_port;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size;
uint32_t bit_rate;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint16_t max_fps;
enum sc_lock_video_orientation lock_video_orientation;
uint8_t rotation;
@ -140,6 +153,9 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool audio;
bool list_encoders;
bool list_displays;
};
extern const struct scrcpy_options scrcpy_options_default;

48
app/src/packet_merger.c Normal file
View File

@ -0,0 +1,48 @@
#include "packet_merger.h"
#include "util/log.h"
void
sc_packet_merger_init(struct sc_packet_merger *merger) {
merger->config = NULL;
}
void
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
free(merger->config);
}
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
if (is_config) {
free(merger->config);
merger->config = malloc(packet->size);
if (!merger->config) {
LOG_OOM();
return false;
}
memcpy(merger->config, packet->data, packet->size);
merger->config_size = packet->size;
} else if (merger->config) {
size_t config_size = merger->config_size;
size_t media_size = packet->size;
if (av_grow_packet(packet, config_size)) {
LOG_OOM();
return false;
}
memmove(packet->data + config_size, packet->data, media_size);
memcpy(packet->data, merger->config, config_size);
free(merger->config);
merger->config = NULL;
// merger->size is meaningless when merger->config is NULL
}
return true;
}

43
app/src/packet_merger.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef SC_PACKET_MERGER_H
#define SC_PACKET_MERGER_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavcodec/avcodec.h>
/**
* Config packets (containing the SPS/PPS) are sent in-band. A new config
* packet is sent whenever a new encoding session is started (on start and on
* device orientation change).
*
* Every time a config packet is received, it must be sent alone (for recorder
* extradata), then concatenated to the next media packet (for correct decoding
* and recording).
*
* This helper reads every input packet and modifies each media packet which
* immediately follows a config packet to prepend the config packet payload.
*/
struct sc_packet_merger {
uint8_t *config;
size_t config_size;
};
void
sc_packet_merger_init(struct sc_packet_merger *merger);
void
sc_packet_merger_destroy(struct sc_packet_merger *merger);
/**
* If the packet is a config packet, then keep its data for later.
* Otherwise (if the packet is a media packet), then if a config packet is
* pending, prepend the config packet to this packet (so the packet is
* modified!).
*/
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
#endif

View File

@ -7,7 +7,7 @@
#include "util/log.h"
bool
receiver_init(struct receiver *receiver, sc_socket control_socket,
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync) {
bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
@ -21,12 +21,12 @@ receiver_init(struct receiver *receiver, sc_socket control_socket,
}
void
receiver_destroy(struct receiver *receiver) {
sc_receiver_destroy(struct sc_receiver *receiver) {
sc_mutex_destroy(&receiver->mutex);
}
static void
process_msg(struct receiver *receiver, struct device_msg *msg) {
process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
@ -51,7 +51,7 @@ process_msg(struct receiver *receiver, struct device_msg *msg) {
}
static ssize_t
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
@ -76,7 +76,7 @@ process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
static int
run_receiver(void *data) {
struct receiver *receiver = data;
struct sc_receiver *receiver = data;
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0;
@ -108,7 +108,7 @@ run_receiver(void *data) {
}
bool
receiver_start(struct receiver *receiver) {
sc_receiver_start(struct sc_receiver *receiver) {
LOGD("Starting receiver thread");
bool ok = sc_thread_create(&receiver->thread, run_receiver,
@ -122,6 +122,6 @@ receiver_start(struct receiver *receiver) {
}
void
receiver_join(struct receiver *receiver) {
sc_receiver_join(struct sc_receiver *receiver) {
sc_thread_join(&receiver->thread, NULL);
}

View File

@ -11,7 +11,7 @@
// receive events from the device
// managed by the controller
struct receiver {
struct sc_receiver {
sc_socket control_socket;
sc_thread thread;
sc_mutex mutex;
@ -20,18 +20,18 @@ struct receiver {
};
bool
receiver_init(struct receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync);
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync);
void
receiver_destroy(struct receiver *receiver);
sc_receiver_destroy(struct sc_receiver *receiver);
bool
receiver_start(struct receiver *receiver);
sc_receiver_start(struct sc_receiver *receiver);
// no receiver_stop(), it will automatically stop on control_socket shutdown
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
void
receiver_join(struct receiver *receiver);
sc_receiver_join(struct sc_receiver *receiver);
#endif

View File

@ -8,8 +8,11 @@
#include "util/log.h"
#include "util/str.h"
/** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
/** Downcast packet sinks to recorder */
#define DOWNCAST_VIDEO(SINK) \
container_of(SINK, struct sc_recorder, video_packet_sink)
#define DOWNCAST_AUDIO(SINK) \
container_of(SINK, struct sc_recorder, audio_packet_sink)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -78,9 +81,7 @@ sc_recorder_get_format_name(enum sc_record_format format) {
}
static bool
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOG_OOM();
@ -92,170 +93,56 @@ sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
return false;
}
return true;
}
static void
sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
static inline void
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
}
static bool
sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = sc_recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
sc_recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
AVPacket *packet) {
AVStream *stream = recorder->ctx->streams[stream_index];
sc_recorder_rescale_packet(stream, packet);
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
}
static int
run_recorder(void *data) {
struct sc_recorder *recorder = data;
static inline bool
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
packet);
}
for (;;) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
sc_mutex_unlock(&recorder->mutex);
struct sc_record_packet *last = recorder->previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet->duration = 100000;
bool ok = sc_recorder_write(recorder, last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
sc_record_packet_delete(last);
}
break;
}
struct sc_record_packet *rec;
sc_queue_take(&recorder->queue, next, &rec);
sc_mutex_unlock(&recorder->mutex);
// recorder->previous is only written from this thread, no need to lock
struct sc_record_packet *previous = recorder->previous;
recorder->previous = rec;
if (!previous) {
// we just received the first packet
continue;
}
// config packets have no PTS, we must ignore them
if (rec->packet->pts != AV_NOPTS_VALUE
&& previous->packet->pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet
previous->packet->duration =
rec->packet->pts - previous->packet->pts;
}
bool ok = sc_recorder_write(recorder, previous->packet);
sc_record_packet_delete(previous);
if (!ok) {
LOGE("Could not record packet");
sc_mutex_lock(&recorder->mutex);
recorder->failed = true;
// discard pending packets
sc_recorder_queue_clear(&recorder->queue);
sc_mutex_unlock(&recorder->mutex);
break;
}
}
if (!recorder->failed) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
}
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = sc_recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name,
recorder->filename);
}
LOGD("Recorder thread ended");
return 0;
static inline bool
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
packet);
}
static bool
sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
goto error_mutex_destroy;
}
sc_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->header_written = false;
recorder->previous = NULL;
sc_recorder_open_output_file(struct sc_recorder *recorder) {
const char *format_name = sc_recorder_get_format_name(recorder->format);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
goto error_cond_destroy;
return false;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOG_OOM();
goto error_cond_destroy;
return false;
}
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx);
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
@ -267,71 +154,441 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
goto error_avformat_free_context;
}
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
goto error_avformat_free_context;
}
LOGD("Starting recorder thread");
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
recorder);
if (!ok) {
LOGE("Could not start recorder thread");
goto error_avio_close;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
error_avio_close:
avio_close(recorder->ctx->pb);
error_avformat_free_context:
avformat_free_context(recorder->ctx);
error_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
return false;
}
static void
sc_recorder_close(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
sc_thread_join(&recorder->thread, NULL);
sc_recorder_close_output_file(struct sc_recorder *recorder) {
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
}
static bool
sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
assert(!recorder->stopped);
while (!recorder->video_codec && !recorder->stopped) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
}
const AVCodec *codec = recorder->video_codec;
sc_mutex_unlock(&recorder->mutex);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
if (codec) {
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
if (!stream) {
return false;
}
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->codec_id = codec->id;
stream->codecpar->format = AV_PIX_FMT_YUV420P;
stream->codecpar->width = recorder->declared_frame_size.width;
stream->codecpar->height = recorder->declared_frame_size.height;
recorder->video_stream_index = stream->index;
}
return true;
}
static bool
sc_recorder_wait_audio_stream(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->audio_codec && !recorder->audio_disabled
&& !recorder->stopped) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
}
if (recorder->audio_disabled) {
// Reset audio flag. From there, the recorder thread may access this
// flag without any mutex.
recorder->audio = false;
}
const AVCodec *codec = recorder->audio_codec;
sc_mutex_unlock(&recorder->mutex);
if (codec) {
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
if (!stream) {
return false;
}
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
stream->codecpar->codec_id = codec->id;
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
stream->codecpar->ch_layout.nb_channels = 2;
#else
stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO;
stream->codecpar->channels = 2;
#endif
stream->codecpar->sample_rate = 48000;
recorder->audio_stream_index = stream->index;
}
return true;
}
static inline bool
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
if (sc_queue_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) {
// The audio queue is empty (when audio is enabled)
return true;
}
// No queue is empty
return false;
}
static bool
sc_recorder_process_header(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) {
// If the recorder is stopped, don't process anything if there are not
// at least video packets
sc_mutex_unlock(&recorder->mutex);
return false;
}
struct sc_record_packet *video_pkt;
sc_queue_take(&recorder->video_queue, next, &video_pkt);
struct sc_record_packet *audio_pkt = NULL;
if (!sc_queue_is_empty(&recorder->audio_queue)) {
assert(recorder->audio);
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
}
sc_mutex_unlock(&recorder->mutex);
int ret = false;
if (video_pkt->packet->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
}
assert(recorder->video_stream_index >= 0);
AVStream *video_stream =
recorder->ctx->streams[recorder->video_stream_index];
bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet);
if (!ok) {
goto end;
}
if (audio_pkt) {
if (audio_pkt->packet->pts != AV_NOPTS_VALUE) {
LOGE("The first audio packet is not a config packet");
goto end;
}
assert(recorder->audio_stream_index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet);
if (!ok) {
goto end;
}
}
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) {
LOGE("Failed to write header to %s", recorder->filename);
goto end;
}
ret = true;
end:
sc_record_packet_delete(video_pkt);
if (audio_pkt) {
sc_record_packet_delete(audio_pkt);
}
return ret;
}
static bool
sc_recorder_process_packets(struct sc_recorder *recorder) {
int64_t pts_origin = AV_NOPTS_VALUE;
bool header_written = sc_recorder_process_header(recorder);
if (!header_written) {
return false;
}
struct sc_record_packet *video_pkt = NULL;
struct sc_record_packet *audio_pkt = NULL;
// We can write a video packet only once we received the next one so that
// we can set its duration (next_pts - current_pts)
struct sc_record_packet *video_pkt_previous = NULL;
bool error = false;
for (;;) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) {
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed
break;
}
if (recorder->audio && !audio_pkt
&& !sc_queue_is_empty(&recorder->audio_queue)) {
// A new packet may be assigned to audio_pkt and be processed
break;
}
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
// If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping.
// If there is no audio, then the audio_queue will remain empty forever
// and audio_pkt will always be NULL.
assert(recorder->audio
|| (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue)));
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
sc_queue_take(&recorder->video_queue, next, &video_pkt);
}
if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) {
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
}
if (recorder->stopped && !video_pkt && !audio_pkt) {
assert(sc_queue_is_empty(&recorder->video_queue));
assert(sc_queue_is_empty(&recorder->audio_queue));
sc_mutex_unlock(&recorder->mutex);
break;
}
assert(video_pkt || audio_pkt); // at least one
sc_mutex_unlock(&recorder->mutex);
// Ignore further config packets (e.g. on device orientation
// change). The next non-config packet will have the config packet
// data prepended.
if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) {
sc_record_packet_delete(video_pkt);
video_pkt = NULL;
}
if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) {
sc_record_packet_delete(audio_pkt);
audio_pkt= NULL;
}
if (pts_origin == AV_NOPTS_VALUE) {
if (!recorder->audio) {
assert(video_pkt);
pts_origin = video_pkt->packet->pts;
} else if (video_pkt && audio_pkt) {
pts_origin =
MIN(video_pkt->packet->pts, audio_pkt->packet->pts);
} else if (recorder->stopped) {
if (video_pkt) {
// The recorder is stopped without audio, record the video
// packets
pts_origin = video_pkt->packet->pts;
} else {
// Fail if there is no video
error = true;
goto end;
}
// If the recorder is stopped while one of the streams has no
// packets, then we must avoid a live-loop and correctly record
// the stream having packets.
pts_origin = video_pkt ? video_pkt->packet->pts
: audio_pkt->packet->pts;
} else {
// We need both video and audio packets to initialize pts_origin
continue;
}
}
assert(pts_origin != AV_NOPTS_VALUE);
if (video_pkt) {
video_pkt->packet->pts -= pts_origin;
video_pkt->packet->dts = video_pkt->packet->pts;
if (video_pkt_previous) {
// we now know the duration of the previous packet
video_pkt_previous->packet->duration =
video_pkt->packet->pts - video_pkt_previous->packet->pts;
bool ok = sc_recorder_write_video(recorder,
video_pkt_previous->packet);
sc_record_packet_delete(video_pkt_previous);
if (!ok) {
LOGE("Could not record video packet");
error = true;
goto end;
}
}
video_pkt_previous = video_pkt;
video_pkt = NULL;
}
if (audio_pkt) {
audio_pkt->packet->pts -= pts_origin;
audio_pkt->packet->dts = audio_pkt->packet->pts;
bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet);
if (!ok) {
LOGE("Could not record audio packet");
error = true;
goto end;
}
sc_record_packet_delete(audio_pkt);
audio_pkt = NULL;
}
}
// Write the last video packet
struct sc_record_packet *last = video_pkt_previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet->duration = 100000;
bool ok = sc_recorder_write_video(recorder, last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
sc_record_packet_delete(last);
}
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
error = false;
}
end:
if (video_pkt) {
sc_record_packet_delete(video_pkt);
}
if (audio_pkt) {
sc_record_packet_delete(audio_pkt);
}
return !error;
}
static bool
sc_recorder_record(struct sc_recorder *recorder) {
bool ok = sc_recorder_open_output_file(recorder);
if (!ok) {
return false;
}
ok = sc_recorder_wait_video_stream(recorder);
if (!ok) {
sc_recorder_close_output_file(recorder);
return false;
}
if (recorder->audio) {
ok = sc_recorder_wait_audio_stream(recorder);
if (!ok) {
sc_recorder_close_output_file(recorder);
return false;
}
}
// If recorder->stopped, process any queued packet anyway
ok = sc_recorder_process_packets(recorder);
sc_recorder_close_output_file(recorder);
return ok;
}
static int
run_recorder(void *data) {
struct sc_recorder *recorder = data;
bool success = sc_recorder_record(recorder);
sc_mutex_lock(&recorder->mutex);
// Prevent the producer to push any new packet
recorder->stopped = true;
// Discard pending packets
sc_recorder_queue_clear(&recorder->video_queue);
sc_mutex_unlock(&recorder->mutex);
if (success) {
const char *format_name = sc_recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name,
recorder->filename);
} else {
LOGE("Recording failed to %s", recorder->filename);
}
LOGD("Recorder thread ended");
recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata);
return 0;
}
static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
assert(codec);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
recorder->video_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
}
static void
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
}
static bool
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
// reject any new packet
sc_mutex_unlock(&recorder->mutex);
return false;
}
@ -343,7 +600,9 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
return false;
}
sc_queue_push(&recorder->queue, next, rec);
rec->packet->stream_index = 0;
sc_queue_push(&recorder->video_queue, next, rec);
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
@ -351,51 +610,191 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
}
static bool
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST(sink);
return sc_recorder_open(recorder, codec);
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
assert(codec);
sc_mutex_lock(&recorder->mutex);
recorder->audio_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
}
static void
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST(sink);
sc_recorder_close(recorder);
sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
}
static bool
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST(sink);
return sc_recorder_push(recorder, packet);
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
// reject any new packet
sc_mutex_unlock(&recorder->mutex);
return false;
}
struct sc_record_packet *rec = sc_record_packet_new(packet);
if (!rec) {
LOG_OOM();
sc_mutex_unlock(&recorder->mutex);
return false;
}
rec->packet->stream_index = 1;
sc_queue_push(&recorder->audio_queue, next, rec);
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
}
static void
sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
assert(!recorder->audio_codec);
LOGW("Audio stream recording disabled");
sc_mutex_lock(&recorder->mutex);
recorder->audio_disabled = true;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
}
bool
sc_recorder_init(struct sc_recorder *recorder,
const char *filename,
enum sc_record_format format,
struct sc_size declared_frame_size) {
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio,
struct sc_size declared_frame_size,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
LOG_OOM();
return false;
}
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
goto error_free_filename;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
goto error_mutex_destroy;
}
ok = sc_cond_init(&recorder->stream_cond);
if (!ok) {
goto error_queue_cond_destroy;
}
recorder->audio = audio;
sc_queue_init(&recorder->video_queue);
sc_queue_init(&recorder->audio_queue);
recorder->stopped = false;
recorder->video_codec = NULL;
recorder->audio_codec = NULL;
recorder->audio_disabled = false;
recorder->video_stream_index = -1;
recorder->audio_stream_index = -1;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
static const struct sc_packet_sink_ops ops = {
.open = sc_recorder_packet_sink_open,
.close = sc_recorder_packet_sink_close,
.push = sc_recorder_packet_sink_push,
assert(cbs && cbs->on_ended);
recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata;
static const struct sc_packet_sink_ops video_ops = {
.open = sc_recorder_video_packet_sink_open,
.close = sc_recorder_video_packet_sink_close,
.push = sc_recorder_video_packet_sink_push,
};
recorder->packet_sink.ops = &ops;
recorder->video_packet_sink.ops = &video_ops;
if (audio) {
static const struct sc_packet_sink_ops audio_ops = {
.open = sc_recorder_audio_packet_sink_open,
.close = sc_recorder_audio_packet_sink_close,
.push = sc_recorder_audio_packet_sink_push,
.disable = sc_recorder_audio_packet_sink_disable,
};
recorder->audio_packet_sink.ops = &audio_ops;
}
return true;
error_queue_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
error_free_filename:
free(recorder->filename);
return false;
}
bool
sc_recorder_start(struct sc_recorder *recorder) {
bool ok = sc_thread_create(&recorder->thread, run_recorder,
"scrcpy-recorder", recorder);
if (!ok) {
LOGE("Could not start recorder thread");
return false;
}
return true;
}
void
sc_recorder_stop(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
}
void
sc_recorder_join(struct sc_recorder *recorder) {
sc_thread_join(&recorder->thread, NULL);
}
void
sc_recorder_destroy(struct sc_recorder *recorder) {
sc_cond_destroy(&recorder->stream_cond);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
free(recorder->filename);
}

View File

@ -20,32 +20,66 @@ struct sc_record_packet {
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
struct sc_recorder {
struct sc_packet_sink packet_sink; // packet sink trait
struct sc_packet_sink video_packet_sink;
struct sc_packet_sink audio_packet_sink;
/* The audio flag is unprotected:
* - it is initialized from sc_recorder_init() from the main thread;
* - it may be reset once from the recorder thread if the audio is
* disabled dynamically.
*
* Therefore, once the recorder thread is started, only the recorder thread
* may access it without data races.
*/
bool audio;
char *filename;
enum sc_record_format format;
AVFormatContext *ctx;
struct sc_size declared_frame_size;
bool header_written;
sc_thread thread;
sc_mutex mutex;
sc_cond queue_cond;
bool stopped; // set on recorder_close()
bool failed; // set on packet write failure
struct sc_recorder_queue queue;
// set on sc_recorder_stop(), packet_sink close or recording failure
bool stopped;
struct sc_recorder_queue video_queue;
struct sc_recorder_queue audio_queue;
// we can write a packet only once we received the next one so that we can
// set its duration (next_pts - current_pts)
// "previous" is only accessed from the recorder thread, so it does not
// need to be protected by the mutex
struct sc_record_packet *previous;
// wake up the recorder thread once the video or audio codec is known
sc_cond stream_cond;
const AVCodec *video_codec;
const AVCodec *audio_codec;
// Instead of providing an audio_codec, the demuxer may notify that the
// stream is disabled if the device could not capture audio
bool audio_disabled;
int video_stream_index;
int audio_stream_index;
const struct sc_recorder_callbacks *cbs;
void *cbs_userdata;
};
struct sc_recorder_callbacks {
void (*on_ended)(struct sc_recorder *recorder, bool success,
void *userdata);
};
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format,
struct sc_size declared_frame_size);
enum sc_record_format format, bool audio,
struct sc_size declared_frame_size,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool
sc_recorder_start(struct sc_recorder *recorder);
void
sc_recorder_stop(struct sc_recorder *recorder);
void
sc_recorder_join(struct sc_recorder *recorder);
void
sc_recorder_destroy(struct sc_recorder *recorder);

View File

@ -13,6 +13,7 @@
# include <windows.h>
#endif
#include "audio_player.h"
#include "controller.h"
#include "decoder.h"
#include "demuxer.h"
@ -40,8 +41,11 @@
struct scrcpy {
struct sc_server server;
struct sc_screen screen;
struct sc_demuxer demuxer;
struct sc_decoder decoder;
struct sc_audio_player audio_player;
struct sc_demuxer video_demuxer;
struct sc_demuxer audio_demuxer;
struct sc_decoder video_decoder;
struct sc_decoder audio_decoder;
struct sc_recorder recorder;
#ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink;
@ -155,9 +159,15 @@ event_loop(struct scrcpy *s) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case EVENT_STREAM_STOPPED:
case SC_EVENT_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_DEMUXER_ERROR:
LOGE("Demuxer error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
@ -176,15 +186,16 @@ await_for_server(bool *connected) {
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
LOGD("User requested to quit");
*connected = false;
if (connected) {
*connected = false;
}
return true;
case EVENT_SERVER_CONNECTION_FAILED:
LOGE("Server connection failed");
case SC_EVENT_SERVER_CONNECTION_FAILED:
return false;
case EVENT_SERVER_CONNECTED:
LOGD("Server connected");
*connected = true;
case SC_EVENT_SERVER_CONNECTED:
if (connected) {
*connected = true;
}
return true;
default:
break;
@ -195,49 +206,47 @@ await_for_server(bool *connected) {
return false;
}
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
static void
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
void *userdata) {
(void) recorder;
(void) userdata;
if (!success) {
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
}
// do not forward others, which are too verbose
return 0;
}
static void
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
}
size_t fmt_len = strlen(fmt);
char *local_fmt = malloc(fmt_len + 10);
if (!local_fmt) {
LOG_OOM();
return;
}
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
free(local_fmt);
}
static void
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
void *userdata) {
(void) demuxer;
(void) userdata;
PUSH_EVENT(EVENT_STREAM_STOPPED);
if (eos) {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
} else {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
}
}
static void
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
void *userdata) {
(void) demuxer;
(void) userdata;
// Contrary to the video demuxer, keep mirroring if only the audio fails.
// 'eos' is true on end-of-stream, including when audio capture is not
// possible on the device (so that scrcpy continue to mirror video without
// failing).
// However, if an audio configuration failure occurs (for example the user
// explicitly selected an unknown audio encoder), 'eos' is false and scrcpy
// must exit.
if (!eos) {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
}
}
static void
@ -245,7 +254,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
}
static void
@ -253,7 +262,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(EVENT_SERVER_CONNECTED);
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
}
static void
@ -266,8 +275,9 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event
}
// Generate a scrcpy id to differentiate multiple running scrcpy instances
static uint32_t
scrcpy_generate_uid() {
scrcpy_generate_scid() {
struct sc_rand rand;
sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side
@ -292,10 +302,12 @@ scrcpy(struct scrcpy_options *options) {
bool server_started = false;
bool file_pusher_initialized = false;
bool recorder_initialized = false;
bool recorder_started = false;
#ifdef HAVE_V4L2
bool v4l2_sink_initialized = false;
#endif
bool demuxer_started = false;
bool video_demuxer_started = false;
bool audio_demuxer_started = false;
#ifdef HAVE_USB
bool aoa_hid_initialized = false;
bool hid_keyboard_initialized = false;
@ -307,28 +319,34 @@ scrcpy(struct scrcpy_options *options) {
struct sc_acksync *acksync = NULL;
uint32_t uid = scrcpy_generate_uid();
uint32_t scid = scrcpy_generate_scid();
struct sc_server_params params = {
.uid = uid,
.scid = scid,
.req_serial = options->serial,
.select_usb = options->select_usb,
.select_tcpip = options->select_tcpip,
.log_level = options->log_level,
.video_codec = options->video_codec,
.audio_codec = options->audio_codec,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,
.tunnel_port = options->tunnel_port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate,
.max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.audio = options->audio,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.codec_options = options->codec_options,
.encoder_name = options->encoder_name,
.video_codec_options = options->video_codec_options,
.audio_codec_options = options->audio_codec_options,
.video_encoder = options->video_encoder,
.audio_encoder = options->audio_encoder,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync,
@ -337,6 +355,8 @@ scrcpy(struct scrcpy_options *options) {
.tcpip_dst = options->tcpip_dst,
.cleanup = options->cleanup,
.power_on = options->power_on,
.list_encoders = options->list_encoders,
.list_displays = options->list_displays,
};
static const struct sc_server_callbacks cbs = {
@ -354,14 +374,27 @@ scrcpy(struct scrcpy_options *options) {
server_started = true;
if (options->list_encoders || options->list_displays) {
bool ok = await_for_server(NULL);
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
goto end;
}
if (options->display) {
sdl_set_hints(options->render_driver);
}
// Initialize SDL video in addition if display is enabled
if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
goto end;
if (options->display) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end;
}
}
sdl_configure(options->display, options->disable_screensaver);
@ -369,15 +402,19 @@ scrcpy(struct scrcpy_options *options) {
// Await for server without blocking Ctrl+C handling
bool connected;
if (!await_for_server(&connected)) {
LOGE("Server connection failed");
goto end;
}
if (!connected) {
// This is not an error, user requested to quit
LOGD("User requested to quit");
ret = SCRCPY_EXIT_SUCCESS;
goto end;
}
LOGD("Server connected");
// It is necessarily initialized here, since the device is connected
struct sc_server_info *info = &s->server.info;
@ -395,41 +432,55 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
struct sc_decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
sc_decoder_init(&s->decoder);
dec = &s->decoder;
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
.on_ended = sc_video_demuxer_on_ended,
};
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
if (options->audio) {
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
.on_ended = sc_audio_demuxer_on_ended,
};
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
&audio_demuxer_cbs, NULL);
}
bool needs_video_decoder = options->display;
bool needs_audio_decoder = options->audio && options->display;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
#endif
if (needs_video_decoder) {
sc_decoder_init(&s->video_decoder, "video");
sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink);
}
if (needs_audio_decoder) {
sc_decoder_init(&s->audio_decoder, "audio");
sc_demuxer_add_sink(&s->audio_demuxer, &s->audio_decoder.packet_sink);
}
struct sc_recorder *rec = NULL;
if (options->record_filename) {
if (!sc_recorder_init(&s->recorder,
options->record_filename,
options->record_format,
info->frame_size)) {
static const struct sc_recorder_callbacks recorder_cbs = {
.on_ended = sc_recorder_on_ended,
};
if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->audio,
info->frame_size, &recorder_cbs, NULL)) {
goto end;
}
rec = &s->recorder;
recorder_initialized = true;
}
av_log_set_callback(av_log_callback);
if (!sc_recorder_start(&s->recorder)) {
goto end;
}
recorder_started = true;
static const struct sc_demuxer_callbacks demuxer_cbs = {
.on_eos = sc_demuxer_on_eos,
};
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
if (dec) {
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
}
if (rec) {
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink);
if (options->audio) {
sc_demuxer_add_sink(&s->audio_demuxer,
&s->recorder.audio_packet_sink);
}
}
struct sc_controller *controller = NULL;
@ -620,7 +671,12 @@ aoa_hid_end:
}
screen_initialized = true;
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink);
if (options->audio) {
sc_audio_player_init(&s->audio_player);
sc_decoder_add_sink(&s->audio_decoder, &s->audio_player.frame_sink);
}
}
#ifdef HAVE_V4L2
@ -630,24 +686,31 @@ aoa_hid_end:
goto end;
}
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
sc_decoder_add_sink(&s->video_decoder, &s->v4l2_sink.frame_sink);
v4l2_sink_initialized = true;
}
#endif
// now we consumed the header values, the socket receives the video stream
// start the demuxer
if (!sc_demuxer_start(&s->demuxer)) {
// start the video demuxer
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
demuxer_started = true;
video_demuxer_started = true;
if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) {
goto end;
}
audio_demuxer_started = true;
}
ret = event_loop(s);
LOGD("quit...");
// Close the window immediately on closing, because screen_destroy() may
// only be called once the demuxer thread is joined (it may take time)
// only be called once the video demuxer thread is joined (it may take time)
sc_screen_hide_window(&s->screen);
end:
@ -674,6 +737,9 @@ end:
if (file_pusher_initialized) {
sc_file_pusher_stop(&s->file_pusher);
}
if (recorder_initialized) {
sc_recorder_stop(&s->recorder);
}
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
}
@ -685,8 +751,12 @@ end:
// now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them
if (demuxer_started) {
sc_demuxer_join(&s->demuxer);
if (video_demuxer_started) {
sc_demuxer_join(&s->video_demuxer);
}
if (audio_demuxer_started) {
sc_demuxer_join(&s->audio_demuxer);
}
#ifdef HAVE_V4L2
@ -705,8 +775,9 @@ end:
}
#endif
// Destroy the screen only after the demuxer is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction
// Destroy the screen only after the video demuxer is guaranteed to be
// finished, because otherwise the screen could receive new frames after
// destruction
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
@ -719,6 +790,9 @@ end:
sc_controller_destroy(&s->controller);
}
if (recorder_started) {
sc_recorder_join(&s->recorder);
}
if (recorder_initialized) {
sc_recorder_destroy(&s->recorder);
}
@ -728,6 +802,10 @@ end:
sc_file_pusher_destroy(&s->file_pusher);
}
if (server_started) {
sc_server_join(&s->server);
}
sc_server_destroy(&s->server);
return ret;

View File

@ -330,7 +330,11 @@ event_watcher(void *data, SDL_Event *event) {
#endif
static bool
sc_screen_frame_sink_open(struct sc_frame_sink *sink) {
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
#ifndef NDEBUG
@ -371,7 +375,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
bool need_new_event;
if (previous_skipped) {
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
// The EVENT_NEW_FRAME triggered for the previous frame will consume
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead, unless the previous event failed
need_new_event = screen->event_failed;
} else {
@ -380,7 +384,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
if (need_new_event) {
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
.type = SC_EVENT_NEW_FRAME,
};
// Post the event on the UI thread
@ -820,7 +824,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
case EVENT_NEW_FRAME: {
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGW("Frame update failed\n");

View File

@ -8,6 +8,7 @@
#include <SDL2/SDL_platform.h>
#include "adb/adb.h"
#include "util/binary.h"
#include "util/file.h"
#include "util/log.h"
#include "util/net_intr.h"
@ -70,8 +71,10 @@ sc_server_params_destroy(struct sc_server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->req_serial);
free((char *) params->crop);
free((char *) params->codec_options);
free((char *) params->encoder_name);
free((char *) params->video_codec_options);
free((char *) params->audio_codec_options);
free((char *) params->video_encoder);
free((char *) params->audio_encoder);
free((char *) params->tcpip_dst);
}
@ -94,8 +97,10 @@ sc_server_params_copy(struct sc_server_params *dst,
COPY(req_serial);
COPY(crop);
COPY(codec_options);
COPY(encoder_name);
COPY(video_codec_options);
COPY(audio_codec_options);
COPY(video_encoder);
COPY(audio_encoder);
COPY(tcpip_dst);
#undef COPY
@ -155,6 +160,24 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
return !stopped;
}
static const char *
sc_server_get_codec_name(enum sc_codec codec) {
switch (codec) {
case SC_CODEC_H264:
return "h264";
case SC_CODEC_H265:
return "h265";
case SC_CODEC_AV1:
return "av1";
case SC_CODEC_OPUS:
return "opus";
case SC_CODEC_AAC:
return "aac";
default:
return NULL;
}
}
static sc_pid
execute_server(struct sc_server *server,
const struct sc_server_params *params) {
@ -198,10 +221,25 @@ execute_server(struct sc_server *server,
cmd[count++] = p; \
}
ADD_PARAM("uid=%08x", params->uid);
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
}
if (!params->audio) {
ADD_PARAM("audio=false");
} else if (params->audio_bit_rate) {
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
}
if (params->video_codec != SC_CODEC_H264) {
ADD_PARAM("video_codec=%s",
sc_server_get_codec_name(params->video_codec));
}
if (params->audio_codec != SC_CODEC_OPUS) {
ADD_PARAM("audio_codec=%s",
sc_server_get_codec_name(params->audio_codec));
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}
@ -231,11 +269,17 @@ execute_server(struct sc_server *server,
if (params->stay_awake) {
ADD_PARAM("stay_awake=true");
}
if (params->codec_options) {
ADD_PARAM("codec_options=%s", params->codec_options);
if (params->video_codec_options) {
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
}
if (params->encoder_name) {
ADD_PARAM("encoder_name=%s", params->encoder_name);
if (params->audio_codec_options) {
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
}
if (params->video_encoder) {
ADD_PARAM("video_encoder=%s", params->video_encoder);
}
if (params->audio_encoder) {
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
}
if (params->power_off_on_close) {
ADD_PARAM("power_off_on_close=true");
@ -256,6 +300,12 @@ execute_server(struct sc_server *server,
// By default, power_on is true
ADD_PARAM("power_on=false");
}
if (params->list_encoders) {
ADD_PARAM("list_encoders=true");
}
if (params->list_displays) {
ADD_PARAM("list_displays=true");
}
#undef ADD_PARAM
@ -370,6 +420,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
server->stopped = false;
server->video_socket = SC_SOCKET_NONE;
server->audio_socket = SC_SOCKET_NONE;
server->control_socket = SC_SOCKET_NONE;
sc_adb_tunnel_init(&server->tunnel);
@ -398,10 +449,9 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
info->frame_size.width = sc_read16be(fields);
info->frame_size.height = sc_read16be(&fields[2]);
return true;
}
@ -414,9 +464,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial;
assert(serial);
bool audio = server->params.audio;
bool control = server->params.control;
sc_socket video_socket = SC_SOCKET_NONE;
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) {
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
@ -424,6 +476,14 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
if (audio) {
audio_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
}
if (control) {
control_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
@ -450,6 +510,18 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
if (audio) {
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
}
if (control) {
// we know that the device is listening, we don't need several
// attempts
@ -476,9 +548,11 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
}
assert(video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE);
server->video_socket = video_socket;
server->audio_socket = audio_socket;
server->control_socket = control_socket;
return true;
@ -490,6 +564,12 @@ fail:
}
}
if (audio_socket != SC_SOCKET_NONE) {
if (!net_close(audio_socket)) {
LOGW("Could not close audio socket");
}
}
if (control_socket != SC_SOCKET_NONE) {
if (!net_close(control_socket)) {
LOGW("Could not close control socket");
@ -672,6 +752,11 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
if (is_already_tcpip) {
// Nothing to do
LOGI("Device already connected via TCP/IP: %s", serial);
server->serial = strdup(serial);
if (!server->serial) {
LOG_OOM();
return false;
}
return true;
}
@ -769,8 +854,27 @@ run_server(void *data) {
assert(serial);
LOGD("Device serial: %s", serial);
ok = push_server(&server->intr, serial);
if (!ok) {
goto error_connection_failed;
}
// If --list-* is passed, then the server just prints the requested data
// then exits.
if (params->list_encoders || params->list_displays) {
sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) {
goto error_connection_failed;
}
sc_process_wait(pid, NULL); // ignore exit code
sc_process_close(pid);
// Wake up await_for_server()
server->cbs->on_connected(server, server->cbs_userdata);
return 0;
}
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
params->uid);
params->scid);
if (r == -1) {
LOG_OOM();
goto error_connection_failed;
@ -778,11 +882,6 @@ run_server(void *data) {
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
assert(server->device_socket_name);
ok = push_server(&server->intr, serial);
if (!ok) {
goto error_connection_failed;
}
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
server->device_socket_name, params->port_range,
params->force_adb_forward);
@ -835,6 +934,11 @@ run_server(void *data) {
assert(server->video_socket != SC_SOCKET_NONE);
net_interrupt(server->video_socket);
if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set
net_interrupt(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) {
// There is no control_socket if --no-control is set
net_interrupt(server->control_socket);
@ -887,7 +991,10 @@ sc_server_stop(struct sc_server *server) {
sc_cond_signal(&server->cond_stopped);
sc_intr_interrupt(&server->intr);
sc_mutex_unlock(&server->mutex);
}
void
sc_server_join(struct sc_server *server) {
sc_thread_join(&server->thread, NULL);
}
@ -896,6 +1003,9 @@ sc_server_destroy(struct sc_server *server) {
if (server->video_socket != SC_SOCKET_NONE) {
net_close(server->video_socket);
}
if (server->audio_socket != SC_SOCKET_NONE) {
net_close(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) {
net_close(server->control_socket);
}

View File

@ -22,21 +22,27 @@ struct sc_server_info {
};
struct sc_server_params {
uint32_t uid;
uint32_t scid;
const char *req_serial;
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
const char *crop;
const char *codec_options;
const char *encoder_name;
const char *video_codec_options;
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
uint16_t max_size;
uint32_t bit_rate;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool audio;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
@ -49,6 +55,8 @@ struct sc_server_params {
bool select_tcpip;
bool cleanup;
bool power_on;
bool list_encoders;
bool list_displays;
};
struct sc_server {
@ -68,6 +76,7 @@ struct sc_server {
struct sc_adb_tunnel tunnel;
sc_socket video_socket;
sc_socket audio_socket;
sc_socket control_socket;
const struct sc_server_callbacks *cbs;
@ -107,6 +116,10 @@ sc_server_start(struct sc_server *server);
void
sc_server_stop(struct sc_server *server);
// join the server thread
void
sc_server_join(struct sc_server *server);
// close and release sockets
void
sc_server_destroy(struct sc_server *server);

View File

@ -5,6 +5,7 @@
#include <assert.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
typedef struct AVFrame AVFrame;
@ -18,7 +19,7 @@ struct sc_frame_sink {
};
struct sc_frame_sink_ops {
bool (*open)(struct sc_frame_sink *sink);
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
void (*close)(struct sc_frame_sink *sink);
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
};

View File

@ -19,9 +19,20 @@ struct sc_packet_sink {
};
struct sc_packet_sink_ops {
/* The codec instance is static, it is valid until the end of the program */
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
void (*close)(struct sc_packet_sink *sink);
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
/*/
* Called when the input stream has been disabled at runtime.
*
* If it is called, then open(), close() and push() will never be called.
*
* It is useful to notify the recorder that the requested audio stream has
* finally been disabled because the device could not capture it.
*/
void (*disable)(struct sc_packet_sink *sink);
};
#endif

View File

@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
(void) userdata;
SDL_Event event;
event.type = EVENT_USB_DEVICE_DISCONNECTED;
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());
@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case EVENT_USB_DEVICE_DISCONNECTED:
case SC_EVENT_USB_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED;
case SDL_QUIT:

26
app/src/util/average.c Normal file
View File

@ -0,0 +1,26 @@
#include "average.h"
#include <assert.h>
void
sc_average_init(struct sc_average *avg, unsigned range) {
avg->range = range;
avg->avg = 0;
avg->count = 0;
}
void
sc_average_push(struct sc_average *avg, float value) {
if (avg->count < avg->range) {
++avg->count;
}
assert(avg->count);
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
}
bool
sc_average_get(struct sc_average *avg, float *value) {
*value = avg->avg;
return avg->count;
}

36
app/src/util/average.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef SC_AVERAGE
#define SC_AVERAGE
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_average {
// Current average value
float avg;
// Target range, to update the average as follow:
// avg = ((range - 1) * avg + new_value) / range
unsigned range;
// Number of values pushed when less than range (count <= range).
// The purpose is to handle the first (range - 1) values properly.
unsigned count;
};
void
sc_average_init(struct sc_average *avg, unsigned range);
/* Push a new value to update the "rolling" average */
void
sc_average_push(struct sc_average *avg, float value);
/* Get the current average value (if available)
*
* An average is available if sc_average_push() has been called at least once.
*/
bool
sc_average_get(struct sc_average *avg, float *value);
#endif

116
app/src/util/bytebuf.c Normal file
View File

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

104
app/src/util/bytebuf.h Normal file
View File

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

View File

@ -4,6 +4,7 @@
# include <windows.h>
#endif
#include <assert.h>
#include <libavformat/avformat.h>
static SDL_LogPriority
log_level_sc_to_sdl(enum sc_log_level level) {
@ -47,6 +48,7 @@ void
sc_set_log_level(enum sc_log_level level) {
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
}
enum sc_log_level
@ -85,3 +87,46 @@ sc_log_windows_error(const char *prefix, int error) {
return true;
}
#endif
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
}
// do not forward others, which are too verbose
return 0;
}
static void
sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
}
size_t fmt_len = strlen(fmt);
char *local_fmt = malloc(fmt_len + 10);
if (!local_fmt) {
LOG_OOM();
return;
}
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl);
free(local_fmt);
}
void
sc_log_configure() {
// Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback);
}

View File

@ -35,4 +35,7 @@ bool
sc_log_windows_error(const char *prefix, int error);
#endif
void
sc_log_configure();
#endif

View File

@ -30,8 +30,8 @@ bool
net_init(void) {
#ifdef _WIN32
WSADATA wsa;
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
int res = WSAStartup(MAKEWORD(1, 1), &wsa);
if (res) {
LOGE("WSAStartup failed with error %d", res);
return false;
}

View File

@ -156,7 +156,10 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
static const struct sc_video_buffer_callbacks cbs = {
.on_new_frame = sc_video_buffer_on_new_frame,
};
@ -336,9 +339,9 @@ sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
}
static bool
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs);
return sc_v4l2_sink_open(vs, ctx);
}
static void

154
app/tests/test_bytebuf.c Normal file
View File

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

View File

@ -46,7 +46,7 @@ static void test_options(void) {
char *argv[] = {
"scrcpy",
"--always-on-top",
"--bit-rate", "5M",
"--video-bit-rate", "5M",
"--crop", "100:200:300:400",
"--fullscreen",
"--max-fps", "30",
@ -75,7 +75,7 @@ static void test_options(void) {
const struct scrcpy_options *opts = &args.opts;
assert(opts->always_on_top);
assert(opts->bit_rate == 5000000);
assert(opts->video_bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen);
assert(opts->max_fps == 30);

View File

@ -90,13 +90,14 @@ static void test_serialize_inject_touch_event(void) {
},
},
.pressure = 1.0f,
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
},
};
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 28);
assert(size == 32);
const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@ -105,7 +106,8 @@ static void test_serialize_inject_touch_event(void) {
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
};
assert(!memcmp(buf, expected, sizeof(expected)));
}

View File

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

View File

@ -16,10 +16,6 @@ cpu = 'i686'
endian = 'little'
[properties]
ffmpeg_avcodec = 'avcodec-58'
ffmpeg_avformat = 'avformat-58'
ffmpeg_avutil = 'avutil-56'
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32'
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@ -16,10 +16,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
ffmpeg_avcodec = 'avcodec-59'
ffmpeg_avformat = 'avformat-59'
ffmpeg_avutil = 'avutil-57'
prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64'
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = 'libusb-1.0.26/MinGW-x64'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -11,7 +11,7 @@
.PHONY: default clean \
test \
build-server \
prepare-deps-win32 prepare-deps-win64 \
prepare-deps \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
@ -62,19 +62,13 @@ build-server:
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
prepare-deps:
@app/prebuilt-deps/prepare-adb.sh
@app/prebuilt-deps/prepare-sdl.sh
@app/prebuilt-deps/prepare-ffmpeg-win32.sh
@app/prebuilt-deps/prepare-ffmpeg.sh
@app/prebuilt-deps/prepare-libusb.sh
prepare-deps-win64:
@app/prebuilt-deps/prepare-adb.sh
@app/prebuilt-deps/prepare-sdl.sh
@app/prebuilt-deps/prepare-ffmpeg-win64.sh
@app/prebuilt-deps/prepare-libusb.sh
build-win32: prepare-deps-win32
build-win32: prepare-deps
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson setup "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
@ -83,7 +77,7 @@ build-win32: prepare-deps-win32
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps-win64
build-win64: prepare-deps
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson setup "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
@ -100,16 +94,16 @@ dist-win32: build-server build-win32
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
@ -119,16 +113,16 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \

View File

@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
android {
namespace 'com.genymobile.scrcpy'
compileSdkVersion 33
defaultConfig {
applicationId "com.genymobile.scrcpy"

View File

@ -1,2 +1,2 @@
<!-- not a real Android application, it is run by app_process manually -->
<manifest package="com.genymobile.scrcpy"/>
<manifest />

View File

@ -0,0 +1,47 @@
package com.genymobile.scrcpy;
import android.media.MediaFormat;
public enum AudioCodec implements Codec {
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
private final int id; // 4-byte ASCII representation of the name
private final String name;
private final String mimeType;
AudioCodec(int id, String name, String mimeType) {
this.id = id;
this.name = name;
this.mimeType = mimeType;
}
@Override
public Type getType() {
return Type.AUDIO;
}
@Override
public int getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public String getMimeType() {
return mimeType;
}
public static AudioCodec findByName(String name) {
for (AudioCodec codec : values()) {
if (codec.name.equals(name)) {
return codec;
}
}
return null;
}
}

View File

@ -0,0 +1,422 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public final class AudioEncoder {
private static class InputTask {
private final int index;
InputTask(int index) {
this.index = index;
}
}
private static class OutputTask {
private final int index;
private final MediaCodec.BufferInfo bufferInfo;
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
this.index = index;
this.bufferInfo = bufferInfo;
}
}
private static final int SAMPLE_RATE = 48000;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
private static final int CHANNELS = 2;
private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private static final int BYTES_PER_SAMPLE = 2;
private static final int BUFFER_MS = 5; // milliseconds
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * BUFFER_MS / 1000;
private final Streamer streamer;
private final int bitRate;
private final List<CodecOption> codecOptions;
private final String encoderName;
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
// So many pending tasks would lead to an unacceptable delay anyway.
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
private Thread thread;
private HandlerThread mediaCodecThread;
private Thread inputThread;
private Thread outputThread;
private boolean ended;
public AudioEncoder(Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(FORMAT);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
builder.setBufferSizeInBytes(minBufferSize);
return builder.build();
}
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, mimeType);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
String key = option.getKey();
Object value = option.getValue();
CodecUtils.setCodecOption(format, key, value);
Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
}
return format;
}
@TargetApi(Build.VERSION_CODES.N)
private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException {
final AudioTimestamp timestamp = new AudioTimestamp();
long previousPts = 0;
long nextPts = 0;
while (!Thread.currentThread().isInterrupted()) {
InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = recorder.read(buffer, BUFFER_SIZE);
if (r < 0) {
throw new IOException("Could not read audio: " + r);
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
} else {
if (nextPts == 0) {
Ln.w("Could not get any audio timestamp");
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
nextPts = pts + durationMs;
if (previousPts != 0 && pts < previousPts) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + 1;
}
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
previousPts = pts;
}
}
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
streamer.writeHeader();
while (!Thread.currentThread().isInterrupted()) {
OutputTask task = outputTasks.take();
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
try {
streamer.writePacket(buffer, task.bufferInfo);
} finally {
mediaCodec.releaseOutputBuffer(task.index, false);
}
}
}
public void start() {
thread = new Thread(() -> {
try {
encode();
} catch (IOException e) {
Ln.e("Audio encoding error", e);
} finally {
Ln.d("Audio encoder stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
end();
}
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
private synchronized void end() {
ended = true;
notify();
}
private synchronized void waitEnded() {
try {
while (!ended) {
wait();
}
} catch (InterruptedException e) {
// ignore
}
}
private static void startWorkaroundAndroid11() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Android 11 requires Apps to be at foreground to record audio.
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
// But Scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
// shell ("com.android.shell").
// If there is an Activity from Android shell running at foreground, then the permission system will believe Scrcpy is also in the
// foreground.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
// Wait for activity to start
SystemClock.sleep(150);
}
}
}
private static void stopWorkaroundAndroid11() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
}
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
MediaCodec mediaCodec = null;
AudioRecord recorder = null;
boolean mediaCodecStarted = false;
boolean recorderStarted = false;
boolean configurationError = false;
try {
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);
mediaCodecThread = new HandlerThread("AudioEncoder");
mediaCodecThread.start();
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
startWorkaroundAndroid11();
try {
recorder = createAudioRecord();
recorder.startRecording();
} catch (UnsupportedOperationException e) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
throw new ConfigurationException("Unsupported audio capture");
}
} finally {
stopWorkaroundAndroid11();
}
recorderStarted = true;
final MediaCodec mediaCodecRef = mediaCodec;
final AudioRecord recorderRef = recorder;
inputThread = new Thread(() -> {
try {
inputThread(mediaCodecRef, recorderRef);
} catch (IOException | InterruptedException e) {
Ln.e("Audio capture error", e);
} finally {
end();
}
});
outputThread = new Thread(() -> {
try {
outputThread(mediaCodecRef);
} catch (InterruptedException e) {
// this is expected on close
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Audio encoding error", e);
}
} finally {
end();
}
});
mediaCodec.start();
mediaCodecStarted = true;
inputThread.start();
outputThread.start();
waitEnded();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
// Notify the error to scrcpy to make it exit
configurationError = true;
} finally {
if (!recorderStarted) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(configurationError);
}
// Cleanup everything (either at the end or on error at any step of the initialization)
if (mediaCodecThread != null) {
Looper looper = mediaCodecThread.getLooper();
if (looper != null) {
looper.quitSafely();
}
}
if (inputThread != null) {
inputThread.interrupt();
}
if (outputThread != null) {
outputThread.interrupt();
}
try {
if (mediaCodecThread != null) {
mediaCodecThread.join();
}
if (inputThread != null) {
inputThread.join();
}
if (outputThread != null) {
outputThread.join();
}
} catch (InterruptedException e) {
// Should never happen
throw new AssertionError(e);
}
if (mediaCodec != null) {
if (mediaCodecStarted) {
mediaCodec.stop();
}
mediaCodec.release();
}
if (recorder != null) {
if (recorderStarted) {
recorder.stop();
}
recorder.release();
}
}
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
if (encoderName != null) {
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
}
}
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
}
private class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
inputTasks.put(new InputTask(index));
} catch (InterruptedException e) {
end();
}
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
try {
outputTasks.put(new OutputTask(index, bufferInfo));
} catch (InterruptedException e) {
end();
}
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Ln.e("MediaCodec error", e);
end();
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// ignore
}
}
}

View File

@ -139,7 +139,7 @@ public final class CleanUp {
builder.start();
}
private static void unlinkSelf() {
public static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {

View File

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

View File

@ -0,0 +1,78 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class CodecUtils {
public static final class DeviceEncoder {
private final Codec codec;
private final MediaCodecInfo info;
DeviceEncoder(Codec codec, MediaCodecInfo info) {
this.codec = codec;
this.info = info;
}
public Codec getCodec() {
return codec;
}
public MediaCodecInfo getInfo() {
return info;
}
}
private CodecUtils() {
// not instantiable
}
public static void setCodecOption(MediaFormat format, String key, Object value) {
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
}
private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) {
List<MediaCodecInfo> result = new ArrayList<>();
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
}
public static List<DeviceEncoder> listVideoEncoders() {
List<DeviceEncoder> encoders = new ArrayList<>();
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (VideoCodec codec : VideoCodec.values()) {
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
encoders.add(new DeviceEncoder(codec, info));
}
}
return encoders;
}
public static List<DeviceEncoder> listAudioEncoders() {
List<DeviceEncoder> encoders = new ArrayList<>();
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (AudioCodec codec : AudioCodec.values()) {
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
encoders.add(new DeviceEncoder(codec, info));
}
}
return encoders;
}
}

View File

@ -0,0 +1,7 @@
package com.genymobile.scrcpy;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {
super(message);
}
}

View File

@ -29,6 +29,7 @@ public final class ControlMessage {
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
@ -60,13 +61,15 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.actionButton = actionButton;
msg.buttons = buttons;
return msg;
}
@ -140,6 +143,10 @@ public final class ControlMessage {
return keycode;
}
public int getActionButton() {
return actionButton;
}
public int getButtons() {
return buttons;
}

View File

@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
@ -140,8 +140,9 @@ public class ControlMessageReader {
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int actionButton = buffer.getInt();
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
}
private ControlMessage parseInjectScrollEvent() {

View File

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.Build;
import android.os.SystemClock;
import android.view.InputDevice;
@ -22,6 +24,8 @@ public class Controller {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private Thread thread;
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
@ -60,7 +64,7 @@ public class Controller {
}
}
public void control() throws IOException {
private void control() throws IOException {
// on start, power on the device
if (powerOn && !Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
@ -80,6 +84,34 @@ public class Controller {
}
}
public void start() {
thread = new Thread(() -> {
try {
control();
} catch (IOException e) {
// this is expected on close
} finally {
Ln.d("Controller stopped");
}
});
thread.start();
sender.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
sender.stop();
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
sender.join();
}
public DeviceMessageSender getSender() {
return sender;
}
@ -99,7 +131,7 @@ public class Controller {
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
@ -179,7 +211,7 @@ public class Controller {
return successCount;
}
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
@ -196,22 +228,23 @@ public class Controller {
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int source;
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);
} else {
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
source = InputDevice.SOURCE_TOUCHSCREEN;
// Buttons must not be set for touch events
buttons = 0;
pointer.setUp(action == MotionEvent.ACTION_UP);
}
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
@ -225,6 +258,62 @@ public class Controller {
}
}
/* If the input device is a mouse (on API >= 23):
* - the first button pressed must first generate ACTION_DOWN;
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
* - the last button released must in addition generate ACTION_UP.
*
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
if (action == MotionEvent.ACTION_DOWN) {
if (actionButton == buttons) {
// First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
// Any button pressed: ACTION_BUTTON_PRESS
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false;
}
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
return true;
}
if (action == MotionEvent.ACTION_UP) {
// Any button released: ACTION_BUTTON_RELEASE
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false;
}
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
if (buttons == 0) {
// Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
return true;
}
}
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);

View File

@ -20,6 +20,9 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
private final LocalSocket audioSocket;
private final FileDescriptor audioFd;
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
@ -27,9 +30,10 @@ public final class DesktopConnection implements Closeable {
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
this.audioSocket = audioSocket;
if (controlSocket != null) {
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
@ -38,6 +42,7 @@ public final class DesktopConnection implements Closeable {
controlOutputStream = null;
}
videoFd = videoSocket.getFileDescriptor();
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
}
private static LocalSocket connect(String abstractName) throws IOException {
@ -46,49 +51,59 @@ public final class DesktopConnection implements Closeable {
return localSocket;
}
private static String getSocketName(int uid) {
if (uid == -1) {
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
private static String getSocketName(int scid) {
if (scid == -1) {
// If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
return SOCKET_NAME_PREFIX;
}
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
}
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(uid);
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(scid);
LocalSocket videoSocket;
LocalSocket videoSocket = null;
LocalSocket audioSocket = null;
LocalSocket controlSocket = null;
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (control) {
try {
try {
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (audio) {
audioSocket = localServerSocket.accept();
}
if (control) {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
}
} else {
videoSocket = connect(socketName);
if (control) {
try {
} else {
videoSocket = connect(socketName);
if (audio) {
audioSocket = connect(socketName);
}
if (control) {
controlSocket = connect(socketName);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
} catch (IOException | RuntimeException e) {
if (videoSocket != null) {
videoSocket.close();
}
if (audioSocket != null) {
audioSocket.close();
}
if (controlSocket != null) {
controlSocket.close();
}
throw e;
}
return new DesktopConnection(videoSocket, controlSocket);
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
}
public void close() throws IOException {
@ -121,6 +136,10 @@ public final class DesktopConnection implements Closeable {
return videoFd;
}
public FileDescriptor getAudioFd() {
return audioFd;
}
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {

View File

@ -61,12 +61,12 @@ public final class Device {
private final boolean supportsInputEvents;
public Device(Options options) {
public Device(Options options) throws ConfigurationException {
displayId = options.getDisplayId();
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
throw new ConfigurationException("Unknown display id: " + displayId);
}
int displayInfoFlags = displayInfo.getFlags();
@ -277,6 +277,26 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {
Ln.e("Could not get physical display ids");
return false;
}
boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
}
return allOk;
}
// Older Android versions, only 1 display
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");

View File

@ -6,6 +6,8 @@ public final class DeviceMessageSender {
private final DesktopConnection connection;
private Thread thread;
private String clipboardText;
private long ack;
@ -24,7 +26,7 @@ public final class DeviceMessageSender {
notify();
}
public void loop() throws IOException, InterruptedException {
private void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) {
String text;
long sequence;
@ -49,4 +51,28 @@ public final class DeviceMessageSender {
}
}
}
public void start() {
thread = new Thread(() -> {
try {
loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
} finally {
Ln.d("Device message sender stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
}

View File

@ -0,0 +1,41 @@
package com.genymobile.scrcpy;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
private static final FakeContext INSTANCE = new FakeContext();
public static FakeContext get() {
return INSTANCE;
}
private FakeContext() {
super(null);
}
@Override
public String getPackageName() {
return PACKAGE_NAME;
}
@Override
public String getOpPackageName() {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
}

View File

@ -48,4 +48,9 @@ public final class IO {
}
return builder.toString();
}
public static boolean isBrokenPipe(IOException e) {
Throwable cause = e.getCause();
return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE;
}
}

View File

@ -1,21 +0,0 @@
package com.genymobile.scrcpy;
public class InvalidDisplayIdException extends RuntimeException {
private final int displayId;
private final int[] availableDisplayIds;
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
super("There is no display having id " + displayId);
this.displayId = displayId;
this.availableDisplayIds = availableDisplayIds;
}
public int getDisplayId() {
return displayId;
}
public int[] getAvailableDisplayIds() {
return availableDisplayIds;
}
}

View File

@ -1,23 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
public class InvalidEncoderException extends RuntimeException {
private final String name;
private final MediaCodecInfo[] availableEncoders;
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
super("There is no encoder having name '" + name + '"');
this.name = name;
this.availableEncoders = availableEncoders;
}
public String getName() {
return name;
}
public MediaCodecInfo[] getAvailableEncoders() {
return availableEncoders;
}
}

View File

@ -0,0 +1,63 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import java.util.List;
public final class LogUtils {
private LogUtils() {
// not instantiable
}
public static String buildVideoEncoderListMessage() {
StringBuilder builder = new StringBuilder("List of video encoders:");
List<CodecUtils.DeviceEncoder> videoEncoders = CodecUtils.listVideoEncoders();
if (videoEncoders.isEmpty()) {
builder.append("\n (none)");
} else {
for (CodecUtils.DeviceEncoder encoder : videoEncoders) {
builder.append("\n --video-codec=").append(encoder.getCodec().getName());
builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'");
}
}
return builder.toString();
}
public static String buildAudioEncoderListMessage() {
StringBuilder builder = new StringBuilder("List of audio encoders:");
List<CodecUtils.DeviceEncoder> audioEncoders = CodecUtils.listAudioEncoders();
if (audioEncoders.isEmpty()) {
builder.append("\n (none)");
} else {
for (CodecUtils.DeviceEncoder encoder : audioEncoders) {
builder.append("\n --audio-codec=").append(encoder.getCodec().getName());
builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'");
}
}
return builder.toString();
}
public static String buildDisplayListMessage() {
StringBuilder builder = new StringBuilder("List of displays:");
DisplayManager displayManager = ServiceManager.getDisplayManager();
int[] displayIds = displayManager.getDisplayIds();
if (displayIds == null || displayIds.length == 0) {
builder.append("\n (none)");
} else {
for (int id : displayIds) {
builder.append("\n --display=").append(id).append(" (");
DisplayInfo displayInfo = displayManager.getDisplayInfo(id);
if (displayInfo != null) {
Size size = displayInfo.getSize();
builder.append(size.getWidth()).append("x").append(size.getHeight());
} else {
builder.append("size unknown");
}
builder.append(")");
}
}
return builder.toString();
}
}

View File

@ -5,10 +5,15 @@ import android.graphics.Rect;
import java.util.List;
public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int uid = -1; // 31-bit non-negative value, or -1
private int scid = -1; // 31-bit non-negative value, or -1
private boolean audio = true;
private int maxSize;
private int bitRate = 8000000;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
private int videoBitRate = 8000000;
private int audioBitRate = 196000;
private int maxFps;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
@ -17,18 +22,25 @@ public class Options {
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private List<CodecOption> codecOptions;
private String encoderName;
private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions;
private String videoEncoder;
private String audioEncoder;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync = true;
private boolean downsizeOnError = true;
private boolean cleanup = true;
private boolean powerOn = true;
private boolean listEncoders;
private boolean listDisplays;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
private boolean sendDeviceMeta = true; // send device name and size
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
public Ln.Level getLogLevel() {
return logLevel;
@ -38,12 +50,20 @@ public class Options {
this.logLevel = logLevel;
}
public int getUid() {
return uid;
public int getScid() {
return scid;
}
public void setUid(int uid) {
this.uid = uid;
public void setScid(int scid) {
this.scid = scid;
}
public boolean getAudio() {
return audio;
}
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() {
@ -54,12 +74,36 @@ public class Options {
this.maxSize = maxSize;
}
public int getBitRate() {
return bitRate;
public VideoCodec getVideoCodec() {
return videoCodec;
}
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() {
return audioCodec;
}
public void setAudioCodec(AudioCodec audioCodec) {
this.audioCodec = audioCodec;
}
public int getVideoBitRate() {
return videoBitRate;
}
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() {
return audioBitRate;
}
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
}
public int getMaxFps() {
@ -126,20 +170,36 @@ public class Options {
this.stayAwake = stayAwake;
}
public List<CodecOption> getCodecOptions() {
return codecOptions;
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
public void setCodecOptions(List<CodecOption> codecOptions) {
this.codecOptions = codecOptions;
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
}
public String getEncoderName() {
return encoderName;
public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions;
}
public void setEncoderName(String encoderName) {
this.encoderName = encoderName;
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() {
return videoEncoder;
}
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() {
return audioEncoder;
}
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
@ -182,6 +242,22 @@ public class Options {
this.powerOn = powerOn;
}
public boolean getListEncoders() {
return listEncoders;
}
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() {
return listDisplays;
}
public void setListDisplays(boolean listDisplays) {
this.listDisplays = listDisplays;
}
public boolean getSendDeviceMeta() {
return sendDeviceMeta;
}
@ -205,4 +281,12 @@ public class Options {
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getSendCodecId() {
return sendCodecId;
}
public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
}

View File

@ -5,18 +5,14 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@ -28,27 +24,26 @@ public class ScreenEncoder implements Device.RotationListener {
// Keep the values in descending order
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private final Device device;
private final Streamer streamer;
private final String encoderName;
private final List<CodecOption> codecOptions;
private final int bitRate;
private final int videoBitRate;
private final int maxFps;
private final boolean sendFrameMeta;
private final boolean downsizeOnError;
private long ptsOrigin;
private boolean firstFrameSent;
private int consecutiveErrors;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.device = device;
this.streamer = streamer;
this.videoBitRate = videoBitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
@ -64,22 +59,15 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false);
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
public void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
streamer.writeHeader();
boolean alive;
try {
do {
@ -93,8 +81,8 @@ public class ScreenEncoder implements Device.RotationListener {
Surface surface = null;
try {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = codec.createInputSurface();
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
@ -102,49 +90,52 @@ public class ScreenEncoder implements Device.RotationListener {
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
mediaCodec.start();
alive = encode(codec, fd);
alive = encode(mediaCodec, streamer);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} catch (MediaCodec.CodecException e) {
Ln.e("Codec error: " + e.getMessage());
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
// For simplicity, handle isTransient() like isRecoverable()
if (e.isRecoverable() || e.isTransient()) {
// Avoid busy-loop if too many errors are generated
SystemClock.sleep(50);
} else if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
alive = true;
mediaCodec.stop();
} catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareDownsizeRetry(device, screenInfo)) {
if (!prepareRetry(device, screenInfo)) {
throw e;
}
Ln.i("Retrying...");
alive = true;
} finally {
codec.reset();
mediaCodec.reset();
if (surface != null) {
surface.release();
}
}
} while (alive);
} finally {
codec.release();
mediaCodec.release();
device.setRotationListener(null);
SurfaceControl.destroyDisplay(display);
}
}
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
if (!downsizeOnError || firstFrameSent) {
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
if (firstFrameSent) {
++consecutiveErrors;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
// Definitively fail
return false;
}
// Wait a bit to increase the probability that retrying will fix the problem
SystemClock.sleep(50);
return true;
}
if (!downsizeOnError) {
// Must fail immediately
return false;
}
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) {
@ -170,30 +161,30 @@ public class ScreenEncoder implements Device.RotationListener {
return 0;
}
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
if (!isConfig) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
consecutiveErrors = 0;
}
streamer.writePacket(codecBuffer, bufferInfo);
}
} finally {
if (outputBufferId >= 0) {
@ -205,74 +196,24 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof;
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = PACKET_FLAG_CONFIG; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
pts |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodecInfo[] listEncoders() {
List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
}
private static MediaCodec createCodec(String encoderName) throws IOException {
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
MediaCodecInfo[] encoders = listEncoders();
throw new InvalidEncoderException(encoderName, encoders);
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
}
}
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
Ln.d("Using encoder: '" + codec.getName() + "'");
return codec;
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
}
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setString(MediaFormat.KEY_MIME, videoMimeType);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
@ -289,7 +230,10 @@ public class ScreenEncoder implements Device.RotationListener {
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
String key = option.getKey();
Object value = option.getValue();
CodecUtils.setCodecOption(format, key, value);
Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
}

View File

@ -1,7 +1,6 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.media.MediaCodecInfo;
import android.os.BatteryManager;
import android.os.Build;
@ -59,52 +58,96 @@ public final class Server {
}
}
private static void scrcpy(Options options) throws IOException {
private static void scrcpy(Options options) throws IOException, ConfigurationException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = options.getCodecOptions();
Thread initThread = startInitThread(options);
int uid = options.getUid();
int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
Workarounds.prepareMainLooper();
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill app info for the AudioRecord to work.
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
if (mustFillAppInfo) {
Workarounds.fillAppInfo();
}
Controller controller = null;
AudioEncoder audioEncoder = null;
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
}
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName(), options.getDownsizeOnError());
Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (control) {
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller.start();
// asynchronous
controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
final Controller controllerRef = controller;
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
}
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
if (audio) {
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
options.getSendFrameMeta());
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder());
audioEncoder.start();
}
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
try {
// synchronous
screenEncoder.streamScreen();
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
}
}
} finally {
Ln.d("Screen streaming stopped");
initThread.interrupt();
if (audioEncoder != null) {
audioEncoder.stop();
}
if (controller != null) {
controller.stop();
}
try {
// synchronous
screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controllerThread != null) {
controllerThread.interrupt();
initThread.join();
if (audioEncoder != null) {
audioEncoder.join();
}
if (deviceMessageSenderThread != null) {
deviceMessageSenderThread.interrupt();
if (controller != null) {
controller.join();
}
} catch (InterruptedException e) {
// ignore
}
}
}
@ -115,32 +158,7 @@ public final class Server {
return thread;
}
private static Thread startController(final Controller controller) {
Thread thread = new Thread(() -> {
try {
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
});
thread.start();
return thread;
}
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(() -> {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
});
thread.start();
return thread;
}
@SuppressWarnings("MethodLength")
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
@ -163,24 +181,46 @@ public final class Server {
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "uid":
int uid = Integer.parseInt(value, 0x10);
if (uid < -1) {
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.setUid(uid);
options.setScid(scid);
break;
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "audio":
boolean audio = Boolean.parseBoolean(value);
options.setAudio(audio);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.setVideoCodec(videoCodec);
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.setAudioCodec(audioCodec);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "bit_rate":
int bitRate = Integer.parseInt(value);
options.setBitRate(bitRate);
case "video_bit_rate":
int videoBitRate = Integer.parseInt(value);
options.setVideoBitRate(videoBitRate);
break;
case "audio_bit_rate":
int audioBitRate = Integer.parseInt(value);
options.setAudioBitRate(audioBitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
@ -214,15 +254,23 @@ public final class Server {
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "codec_options":
List<CodecOption> codecOptions = CodecOption.parse(value);
options.setCodecOptions(codecOptions);
case "video_codec_options":
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
options.setVideoCodecOptions(videoCodecOptions);
break;
case "encoder_name":
case "audio_codec_options":
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
options.setAudioCodecOptions(audioCodecOptions);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.setEncoderName(value);
options.setVideoEncoder(value);
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.setAudioEncoder(value);
}
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
@ -243,6 +291,14 @@ public final class Server {
boolean powerOn = Boolean.parseBoolean(value);
options.setPowerOn(powerOn);
break;
case "list_encoders":
boolean listEncoders = Boolean.parseBoolean(value);
options.setListEncoders(listEncoders);
break;
case "list_displays":
boolean listDisplays = Boolean.parseBoolean(value);
options.setListDisplays(listDisplays);
break;
case "send_device_meta":
boolean sendDeviceMeta = Boolean.parseBoolean(value);
options.setSendDeviceMeta(sendDeviceMeta);
@ -255,12 +311,17 @@ public final class Server {
boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte);
break;
case "send_codec_id":
boolean sendCodecId = Boolean.parseBoolean(value);
options.setSendCodecId(sendCodecId);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.setSendDeviceMeta(false);
options.setSendFrameMeta(false);
options.setSendDummyByte(false);
options.setSendCodecId(false);
}
break;
default:
@ -288,38 +349,35 @@ public final class Server {
return new Rect(x, y, x + width, y + height);
}
private static void suggestFix(Throwable e) {
if (e instanceof InvalidDisplayIdException) {
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
int[] displayIds = idie.getAvailableDisplayIds();
if (displayIds != null && displayIds.length > 0) {
Ln.e("Try to use one of the available display ids:");
for (int id : displayIds) {
Ln.e(" scrcpy --display " + id);
}
}
} else if (e instanceof InvalidEncoderException) {
InvalidEncoderException iee = (InvalidEncoderException) e;
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
}
}
}
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
suggestFix(e);
});
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options);
if (options.getListEncoders() || options.getListDisplays()) {
if (options.getCleanup()) {
CleanUp.unlinkSelf();
}
if (options.getListEncoders()) {
Ln.i(LogUtils.buildVideoEncoderListMessage());
Ln.i(LogUtils.buildAudioEncoderListMessage());
}
if (options.getListDisplays()) {
Ln.i(LogUtils.buildDisplayListMessage());
}
// Just print the requested data, do not mirror
return;
}
try {
scrcpy(options);
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
}
}
}

View File

@ -0,0 +1,129 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class Streamer {
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
private final FileDescriptor fd;
private final Codec codec;
private final boolean sendCodecId;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
this.fd = fd;
this.codec = codec;
this.sendCodecId = sendCodecId;
this.sendFrameMeta = sendFrameMeta;
}
public Codec getCodec() {
return codec;
}
public void writeHeader() throws IOException {
if (sendCodecId) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
buffer.flip();
IO.writeFully(fd, buffer);
}
}
public void writeDisableStream(boolean error) throws IOException {
// Writing a specific code as codec-id means that the device disables the stream
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only
// code 1: a configuration error occurred, scrcpy must be stopped
byte[] code = new byte[4];
if (error) {
code[3] = 1;
}
IO.writeFully(fd, code, 0, code.length);
}
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
if (codec == AudioCodec.OPUS) {
fixOpusConfigPacket(codecBuffer, bufferInfo);
}
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long ptsAndFlags;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
} else {
ptsAndFlags = bufferInfo.presentationTimeUs;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(ptsAndFlags);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
// Here is an example of the config packet received for an OPUS stream:
//
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
// 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....|
// 00000020 00 00 00 |... |
// ------------------------------------------------------------------------------
// 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....|
// 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS|
// 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............|
// 00000050 00 00 00 |...|
//
// Each "section" is prefixed by a 64-bit ID and a 64-bit length.
//
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
if (!isConfig) {
return;
}
if (buffer.remaining() < 16) {
throw new IOException("Not enough data in OPUS config packet");
}
long id = buffer.getLong();
if (id != AOPUSHDR) {
throw new IOException("OPUS header not found");
}
long sizeLong = buffer.getLong();
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
}
int size = (int) sizeLong;
if (buffer.remaining() < size) {
throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")");
}
// Set the buffer to point to the OPUS header slice
buffer.limit(buffer.position() + size);
}
}

View File

@ -0,0 +1,50 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.media.MediaFormat;
public enum VideoCodec implements Codec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
@SuppressLint("InlinedApi") // introduced in API 21
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
private final int id; // 4-byte ASCII representation of the name
private final String name;
private final String mimeType;
VideoCodec(int id, String name, String mimeType) {
this.id = id;
this.name = name;
this.mimeType = mimeType;
}
@Override
public Type getType() {
return Type.VIDEO;
}
@Override
public int getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public String getMimeType() {
return mimeType;
}
public static VideoCodec findByName(String name) {
for (VideoCodec codec : values()) {
if (codec.name.equals(name)) {
return codec;
}
}
return null;
}
}

View File

@ -2,14 +2,12 @@ package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
private Workarounds() {
@ -50,7 +48,7 @@ public final class Workarounds {
Object appBindData = appBindDataConstructor.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = "com.genymobile.scrcpy";
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
// appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
@ -62,11 +60,10 @@ public final class Workarounds {
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
// Context ctx = activityThread.getSystemContext();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Application app = Instrumentation.newApplication(Application.class, ctx);
Application app = Application.class.newInstance();
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");

View File

@ -1,8 +1,14 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
@ -10,12 +16,15 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod;
private Method startActivityAsUserWithFeatureMethod;
private Method forceStopPackageMethod;
public ActivityManager(IInterface manager) {
this.manager = manager;
@ -42,16 +51,17 @@ public class ActivityManager {
return removeContentProviderExternalMethod;
}
@TargetApi(Build.VERSION_CODES.Q)
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (getContentProviderExternalMethodNewVersion) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
args = new Object[]{name, FakeContext.ROOT_UID, token, null};
} else {
// old version
args = new Object[]{name, ServiceManager.USER_ID, token};
args = new Object[]{name, FakeContext.ROOT_UID, token};
}
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args);
@ -84,4 +94,55 @@ public class ActivityManager {
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
}
private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException {
if (startActivityAsUserWithFeatureMethod == null) {
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
Class<?> profilerInfo = Class.forName("android.app.ProfilerInfo");
startActivityAsUserWithFeatureMethod = manager.getClass()
.getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class,
IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class);
}
return startActivityAsUserWithFeatureMethod;
}
@SuppressWarnings("ConstantConditions")
public int startActivityAsUserWithFeature(Intent intent) {
try {
Method method = getStartActivityAsUserWithFeatureMethod();
return (int) method.invoke(
/* this */ manager,
/* caller */ null,
/* callingPackage */ FakeContext.PACKAGE_NAME,
/* callingFeatureId */ null,
/* intent */ intent,
/* resolvedType */ null,
/* resultTo */ null,
/* resultWho */ null,
/* requestCode */ 0,
/* startFlags */ 0,
/* profilerInfo */ null,
/* bOptions */ null,
/* userId */ /* UserHandle.USER_CURRENT */ -2);
} catch (Throwable e) {
Ln.e("Could not invoke method", e);
return 0;
}
}
private Method getForceStopPackageMethod() throws NoSuchMethodException {
if (forceStopPackageMethod == null) {
forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
}
return forceStopPackageMethod;
}
public void forceStopPackage(String packageName) {
try {
Method method = getForceStopPackageMethod();
method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
} catch (Throwable e) {
Ln.e("Could not invoke method", e);
}
}
}

View File

@ -1,5 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
@ -58,22 +59,22 @@ public class ClipboardManager {
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
if (alternativeMethod) {
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
}
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
}
@ -106,11 +107,11 @@ public class ClipboardManager {
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
}

View File

@ -1,9 +1,12 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import android.annotation.SuppressLint;
import android.content.AttributionSource;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@ -51,11 +54,10 @@ public class ContentProvider implements Closeable {
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
try {
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0;
} catch (NoSuchMethodException | ClassNotFoundException e0) {
} else {
// old versions
try {
callMethod = provider.getClass()
@ -75,40 +77,29 @@ public class ContentProvider implements Closeable {
return callMethod;
}
@SuppressLint("PrivateApi")
private Object getAttributionSource()
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (attributionSource == null) {
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
}
return attributionSource;
}
private Bundle call(String callMethod, String arg, Bundle extras)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
try {
Method method = getCallMethod();
Object[] args;
switch (callMethodVersion) {
case 0:
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
break;
case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 2:
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
break;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
} else {
switch (callMethodVersion) {
case 1:
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 2:
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
break;
}
}
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
throw e;
}
@ -147,7 +138,7 @@ public class ContentProvider implements Closeable {
public String getValue(String table, String key) throws SettingsException {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
try {
Bundle bundle = call(method, key, arg);
if (bundle == null) {
@ -163,7 +154,7 @@ public class ContentProvider implements Closeable {
public void putValue(String table, String key, String value) throws SettingsException {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
try {
call(method, key, arg);

View File

@ -3,6 +3,7 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.view.InputEvent;
import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -17,6 +18,7 @@ public final class InputManager {
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
public InputManager(android.hardware.input.InputManager manager) {
this.manager = manager;
@ -56,4 +58,22 @@ public final class InputManager {
return false;
}
}
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
if (setActionButtonMethod == null) {
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
}
return setActionButtonMethod;
}
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
try {
Method method = getSetActionButtonMethod();
method.invoke(motionEvent, actionButton);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot set action button on MotionEvent", e);
return false;
}
}
}

View File

@ -10,9 +10,6 @@ import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private static final Method GET_SERVICE_METHOD;
static {
try {

View File

@ -30,6 +30,8 @@ public final class SurfaceControl {
private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod;
private static Method getPhysicalDisplayTokenMethod;
private static Method getPhysicalDisplayIdsMethod;
private SurfaceControl() {
// only static methods
@ -98,7 +100,6 @@ public final class SurfaceControl {
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@ -114,6 +115,40 @@ public final class SurfaceControl {
}
}
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
if (getPhysicalDisplayTokenMethod == null) {
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
}
return getPhysicalDisplayTokenMethod;
}
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
try {
Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
if (getPhysicalDisplayIdsMethod == null) {
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
}
return getPhysicalDisplayIdsMethod;
}
public static long[] getPhysicalDisplayIds() {
try {
Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);

View File

@ -94,7 +94,8 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
byte[] packet = bos.toByteArray();
@ -112,6 +113,7 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
}