Compare commits

...
Author SHA1 Message Date
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Simon ChanandRomain Vimont 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
Romain VimontandSimon Chan 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 8e32d15e6c Pass AVCodecContext to frame sinks
Frame consumers may need details about the frame format.
2023-02-28 12:35:08 +01:00
Romain Vimont 7f2989e1d5 Add an audio decoder 2023-02-28 12:35:08 +01:00
Romain Vimont da57902dd5 Give a name to decoder instances
This will be useful in logs.
2023-02-28 12:35:08 +01:00
Romain Vimont 28701090f6 Rename decoder to video_decoder 2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 30b8429752 Add --list-device-displays 2023-02-28 12:35:08 +01:00
Romain Vimont 38e5dafba6 Move log message helpers to LogUtils
This class will also contain other log helpers.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 55f4c42f19 Add --list-encoders
Add an option to list the device encoders properly.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont e39ad0f695 Add --audio-encoder
Similar to --video-encoder, but for audio.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont aa8ed923f0 Add --audio-codec-options
Similar to --video-codec-options, but for audio.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 7f50ed2458 Add support for AAC audio codec
Add option --audio-codec=aac.
2023-02-28 12:35:08 +01:00
Romain Vimont 9187472014 Add --audio-codec
Introduce the selection mechanism. Alternative codecs will be added
later.
2023-02-28 12:35:08 +01:00
Romain Vimont a331c2c653 Add --audio-bit-rate
Add an option to configure the audio bit-rate.
2023-02-28 12:35:08 +01:00
Romain Vimont f75dc2e477 Disable MethodLength checkstyle on createOptions()
This method will grow as needed to initialize options.
2023-02-28 12:35:08 +01:00
Romain Vimont 8a5be9e2a6 Rename --encoder to --video-encoder
This prepares the introduction of --audio-encoder.
2023-02-28 12:35:08 +01:00
Romain Vimont 705d69aaea Rename --codec-options to --video-codec-options
This prepares the introduction of --audio-codec-options.
2023-02-28 12:35:08 +01:00
Romain Vimont ad51a2b411 Rename --bit-rate to --video-bit-rate
This prepares the introduction of --audio-bit-rate.
2023-02-28 12:35:08 +01:00
Romain Vimont 7581dc10d3 Rename --codec to --video-codec
This prepares the introduction of --audio-codec.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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 <https://android.googlesource.com/platform/frameworks/base/+/4feeee88911503cc365b223a712437abcd91c94f%5E%21/>
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 93e86a5661 Rename video-specific variables in recorder
This paves the way to add audio-specific variables.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 98ece15421 Give a name to demuxer instances
This will be useful in logs.
2023-02-28 12:35:08 +01:00
Romain Vimont 8050de011c Rename demuxer to video_demuxer
There will be another demuxer instance for audio.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Simon ChanandRomain Vimont 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
Simon ChanandRomain Vimont 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
Simon ChanandRomain Vimont 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
Romain Vimont 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
Romain Vimont 504793b5c9 Use shell package name for workarounds
For consistency.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 7c0ee70261 Use PACKAGE_NAME from FakeContext
Remove duplicated constant.
2023-02-28 12:35:08 +01:00
Romain VimontandSimon Chan 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
Simon ChanandRomain Vimont 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
Romain Vimont 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
Romain Vimont 7b3a39bdc7 Rename "codec" variable to "mediaCodec"
This will allow to use "codec" for the Codec type.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 5b01457364 Move screen encoder initialization
This prepares further refactors.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 97ae0a2d13 Reorder initialization
Initialize components in the pipeline order: demuxer first, decoder and
recorder second.
2023-02-28 12:35:08 +01:00
Romain Vimont f1a4349834 Refactor recorder logic
Process the initial config packet (necessary to write the header)
separately.
2023-02-28 12:35:08 +01:00
Romain Vimont a95bfe4f01 Move last packet recording
Write the last packet at the end.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 6b669d2dba Inline packet_sink impl in recorder
Remove useless wrappers.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont d3adda176b Move previous packet to a local variable
It is only used from run_recorder().
2023-02-28 12:35:08 +01:00
Romain Vimont e7fa099be4 Move pts_origin to a local variable
It is only used from run_recorder().
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 9233f1990e Fix --no-clipboard-autosync bash completion
Fix typo.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont bf7c0f2c33 Move FFmpeg callback initialization
Configure FFmpeg log redirection on start from a log helper.
2023-02-28 12:35:08 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 6b422e21bf Fix error message on icon loading failure 2023-02-27 20:51:54 +01:00
Romain Vimont 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
Romain Vimont 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 <https://github.com/FFmpeg/FFmpeg/blob/891ed24f77da99c6d41bb7c116ba5925e3206ce2/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
Romain Vimont 0cea7fb24c Fix WSAStartup() error check on Windows 2023-02-27 20:51:35 +01:00
Romain Vimont 3d10fbd9b4 Fix --bit-rate option in bash completion script
The option is --bit-rate, not --bitrate.
2023-02-21 19:50:01 +01:00
Romain Vimont 3e3756a323 Add auto-completion for --codec option
Add missing command to bash and zsh completion scripts.
2023-02-21 19:48:28 +01:00
Romain Vimont 5d6bcc5966 Use enum for long options constants
This avoids to manually assign values.
2023-02-21 19:47:27 +01:00
Romain Vimont 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
Romain Vimont 0a151b96fe Accept muxing AV1 into MP4 container
MP4 supports AV1.

Refs d2dce51038
2023-02-20 20:49:56 +01:00
Romain Vimont 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
Romain Vimont d5dff239c8 Suggest commands with an explicit '=' 2023-02-19 16:18:19 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 25e2eb7d7c Document default video codec
Mention the default option value, like for other commands.
2023-02-18 19:10:25 +01:00
Romain Vimont 280a9afda8 Fix command-line help typo 2023-02-18 18:11:34 +01:00
Romain Vimont e91618586c Prefix receiver by sc_
Like all other components in scrcpy.
2023-02-18 09:37:31 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont f03f32267e Remove unused parser
Since 1c02b58412, the parser is not used
anymore.
2023-02-10 23:18:11 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 4f9e9c6619 Prefix UI events constants by SC_ 2023-02-10 18:55:43 +01:00
Romain Vimont 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
Romain Vimont 230b8274b9 Fix error return value
The function returns an enum AVCodecID, not a bool.
2023-02-10 18:55:43 +01:00
Romain Vimont 40866ddc10 Fix demuxer error message
The message applies to all packets, not only config packets.
2023-02-10 18:55:39 +01:00
Romain Vimont bd56c0abf7 Remove unused codec context
The demuxer does not need any codec context.
2023-02-10 18:46:01 +01:00
Romain Vimont 6524e90c68 Remove unused constant
This line was committed by error.

Refs 3e517cd40e
2023-02-07 23:11:42 +01:00
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont f70f6cdd3e Simplify server info initialization
Use sc_read16be() to read 16-bit integer fields.
2023-02-03 12:31:28 +01:00
Romain Vimont 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
Romain Vimont 3aac74e9e9 Move variable assignment
Computing eof flag is not necessary if rotation changed.
2023-02-03 12:31:28 +01:00
Romain Vimont 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
Romain Vimont 36d656e91f Improve workarounds call comments 2023-02-03 12:31:28 +01:00
Romain VimontandSimon Chan 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
Romain Vimont 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
Romain Vimont 6a07e3d470 Fix manpage formatting
Only the option arguments must be underlined.
2023-02-03 12:31:28 +01:00
Simon Chan 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
Simon Chan 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
Simon ChanandRomain Vimont 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
Romain Vimont 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
Romain Vimont 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
Romain Vimont a9b2697f3e Move local variables declarations
This makes it clear that these local variables are only passed to
setDisplaySurface().
2023-01-27 22:43:16 +01:00
Romain Vimont b53d2c66e0 Remove useless setSize() method
Inline its content. It makes the logic of internalStreamScreen() more
readable (it reduces the number of indirections).
2023-01-27 22:39:28 +01:00
Romain Vimont 6cccf3ab2a Remove useless configure() method
Inline its single call.
2023-01-27 22:38:37 +01:00
Romain Vimont 52f85fd6f1 Keep the same MediaCodec instance across sessions
Calling codec.reset() is sufficient.
2023-01-27 22:31:09 +01:00
Romain Vimont 91c69ad95c Remove useless destroyDisplay() method
The method made exactly one simple call. Just make the call directly.
2023-01-27 22:28:11 +01:00
Romain Vimont 75d7c01a0c Keep the same display binder across sessions
Do not destroy/recreate the display when starting a new encoding session
(on device rotation for example).
2023-01-27 22:26:01 +01:00
Romain Vimont 74d32e612d Terminate loop explicitly on interrupted
Make explicit that the loop terminates when the current thread is
interrupted.
2023-01-27 22:20:35 +01:00
Romain Vimont bdba554118 Use Java lambdas where possible 2023-01-27 22:16:36 +01:00
Romain Vimont 234ad7ee78 Support Java lambdas in build_without_gradle.sh
Building Java source code using lambdas requires core-lambda-stubs.jar.

Refs #3657 <https://github.com/Genymobile/scrcpy/issues/3657>
2023-01-27 22:08:25 +01:00
Romain Vimont 8cbbcc939f Add missing final modifiers 2023-01-27 22:08:17 +01:00
Romain Vimont b22810b17c Use try-with-resources
Replace an explicit try-finally by a try-with-resources block.
2023-01-27 21:59:26 +01:00
Romain Vimont 4315be1648 Use random name for device socket
For the initial connection between the device and the computer, an adb
tunnel is established (with "adb reverse" or "adb forward").

The device-side of the tunnel is a local socket having the hard-coded
name "scrcpy". This may cause issues when several scrcpy instances are
started in a few seconds for the same device, since they will try to
bind the same name.

To avoid conflicts, make the client generate a random UID, and append
this UID to the local socket name ("scrcpy_01234567").
2023-01-27 21:51:59 +01:00
Romain Vimont 74e3f8b253 Add random util
Add a user-friendly tool to generate random numbers.
2023-01-27 19:26:19 +01:00
Romain Vimont 059ec45f82 Add jrand48()/nrand48() compat functions
These functions are not available on all platforms.
2023-01-26 18:11:23 +01:00
Romain Vimont e6cd42355b Use separate gen dir to build without gradle
The generated source files were written to the classes dir. Use a
separate gen dir instead.
2023-01-26 10:44:31 +01:00
Romain Vimont bf8696d02e Avoid unnecessary copy on config packets demuxing
Use av_packet_ref() to reference the packet without copy.

This also simplifies the logic, by making the "offset" variable and the
memcpy() call local to the if-block.
2023-01-02 16:18:23 +01:00
Romain Vimont d8c2fe6ef2 Revert "Remove continuous resizing workaround for Windows"
This reverts commit 18082f6069.

I can't reproduce, but it seems the workaround improves the behavior on
some Windows versions.

Fixes #3640 <https://github.com/Genymobile/scrcpy/issues/3640>
Refs #3458 <https://github.com/Genymobile/scrcpy/issues/3458>
2022-12-26 12:42:59 +01:00
Romain Vimont 54c7baceac Use "meson setup" from install_release.sh
Refs 64821466a1
2022-12-22 13:07:07 +01:00
Romain Vimont 4c43784fd1 Update links to v1.25 2022-12-22 12:44:01 +01:00
Romain Vimont fe21158c20 Bump version to 1.25 2022-12-22 12:33:29 +01:00
Romain Vimont 8e0c899218 Upgrade FFmpeg (5.1.2) for Windows 64-bit
Use the latest version of FFmpeg in Windows 64-bit releases.
2022-12-22 12:33:08 +01:00
Romain Vimont 725a922271 Upgrade SDL (2.26.1) for Windows
Include the latest version of SDL in Windows releases.
2022-12-22 12:29:08 +01:00
Romain Vimont b5773a6fe8 Upgrade platform-tools (33.0.3) for Windows
Include the latest version of adb in Windows releases.
2022-12-22 12:29:08 +01:00
Romain Vimont 67fb457dcb Merge branch 'master' into dev 2022-12-22 12:29:00 +01:00
Pawel JasinskiandRomain Vimont c7b1d0ea9a Force mouse source when --forward-all-clicks
Right click and middle click require the source device to be a mouse,
not a touchscreen. Therefore, the source device was changed only when a
button other than the primary button was pressed (see
adc547fa6e).

However, this led to inconsistencies between the ACTION_DOWN when a
secondary button is pressed (with a mouse as source device) and the
matching ACTION_UP when the secondary button is released (with a
touchscreen as source device, because then there is no button pressed).

To avoid the problem in all cases, force a mouse as source device when
--forward-all-clicks is set.

Concretely, for mouse events in --forward-all-clicks mode:
 - device source is set to InputDevice.SOURCE_MOUSE;
 - motion event toolType is set to MotionEvent.TOOL_TYPE_MOUSE;

Otherwise (when --forward-all-clicks is unset, or for real touch
events), finger events are injected:
 - device source is set to InputDevice.SOURCE_TOUCHSCREEN;
 - motion event toolType is set to MotionEvent.TOOL_TYPE_FINGER.

Fixes #3568 <https://github.com/Genymobile/scrcpy/issues/3568>
PR #3579 <https://github.com/Genymobile/scrcpy/pull/3579>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-12-22 11:26:26 +01:00
Romain Vimont 18082f6069 Remove continuous resizing workaround for Windows
It turns out that the workaround only worked for MacOS.

Refs #3458 <https://github.com/Genymobile/scrcpy/issues/3458>
Refs SDL/#1059 <https://github.com/libsdl-org/SDL/issues/1059>
2022-12-21 22:06:43 +01:00
Romain Vimont 8b38b11875 Add parent directory in release zipfile
This avoids to mistakenly extract all the files in the current
directory.
2022-12-21 13:31:18 +01:00
Romain Vimont 64821466a1 Use "meson setup"
This fixes the following warning:

> WARNING: Running the setup command as `meson [options]` instead of
> `meson setup [options]` is ambiguous and deprecated.
2022-12-21 13:29:27 +01:00
Romain Vimont 82cb8ab870 Adapt ClipboardManager for Android 13
A new "attributionTag" parameter has been added to the methods
getPrimaryClip(), setPrimaryClip() and addPrimaryClipChangedListener()
of IClipboard.aidl.

Refs <https://android.googlesource.com/platform/frameworks/base.git/+/0e3e509b3bf4d561c26d87213387a290dfd688e3%5E%21/>

Fixes #3497 <https://github.com/Genymobile/scrcpy/issues/3497>
2022-12-21 13:28:22 +01:00
Romain Vimont b51841e85d Upgrade junit to 4.13.2 2022-12-21 13:28:22 +01:00
Romain Vimont bd1deffa70 Use current adb port (if any) for --tcpip
If the current adb port is not 5555 (typically 0 because it is not in
TCP/IP mode), --tcpip automatically executes (among other commands):

    adb tcpip 5555

In case adb was already listening on another port, this command forced
to listen on 5555, and the connection should still succeed.

But this reconfiguration might be inconvenient for the user. If adb is
already in TCP/IP mode, use the current enabled port without
reconfiguration.

Fixes #3591 <https://github.com/Genymobile/scrcpy/issues/3591>
2022-12-02 19:09:53 +01:00
Romain Vimont 6469b55861 Fix CommandParserTest code style
Make checkstyle happy.
2022-11-24 09:27:10 +01:00
Romain Vimont c00a9ead5e Always use --key=value in README
Mandatory arguments may be passed in either of these two forms:
 1. --key value
 2. --key=value

Optional argument may only be passed in the second form.

For consistency, always document using --key=value.

Refs f76fe2c0d4
2022-11-17 09:27:05 +01:00
SeungHoon Han 597703b62e Fix DisplayInfo parsing for Android Q
The DisplayInfo dump format has slightly changed in AOSP:
<https://android.googlesource.com/platform/frameworks/base.git/+/1039ea50f3e1944f5d670cfdeadfdb17b3442ed7>

PR #3573 <https://github.com/Genymobile/scrcpy/pull/3573>
Ref #3416 <https://github.com/Genymobile/scrcpy/pull/3416>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-11-12 18:15:45 +01:00
Yu-Chen Lin 48bb6f2ea8 Support wchar_t in argv for Windows
PR #3547 <https://github.com/Genymobile/scrcpy/pull/3547>
Fixes #2932 <https://github.com/Genymobile/scrcpy/issues/2932>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-23 23:45:00 +02:00
Yu-Chen Lin d71587e39b Avoid string concatenation in crossfiles
This feature is not supported on older meson versions:

    ERROR: Malformed value in cross file variable prebuilt_libusb.

Refs <https://github.com/mesonbuild/meson/issues/3878>
PR #3546 <https://github.com/Genymobile/scrcpy/pull/3546>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-23 12:31:44 +02:00
Romain Vimont b62424a98a Build log.c for test_cli
On Windows, sc_log_windows_error() is called from net.c, so log.c must
also be compiled.

Fixes #3542 <https://github.com/Genymobile/scrcpy/issues/3542>
2022-10-19 15:17:43 +02:00
Romain Vimont ffc7b91693 Add missing include <string.h> for strlen() 2022-10-19 15:14:56 +02:00
Romain Vimont cb46e4a64a Add missing include <string.h> for memmove() 2022-10-19 15:13:55 +02:00
Anima C13 16e2c1ce26 Add -s auto-completion for zsh
Fixes #3522 <https://github.com/Genymobile/scrcpy/pull/3522>
PR #3523 <https://github.com/Genymobile/scrcpy/pull/3523>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-12 13:24:43 +02:00
Anima C13 1bfbadef96 Add -s auto-completion for bash
Fixes #3522 <https://github.com/Genymobile/scrcpy/pull/3522>
PR #3523 <https://github.com/Genymobile/scrcpy/pull/3523>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-10-12 13:24:43 +02:00
Romain Vimont 40644994e8 Make ServiceManager and Settings methods static
There were exactly one instance of ServiceManager and Settings, stored
in Device.

Since a Device instance is not created by the CleanUp executable, it was
not straightforward to call wrapper methods on cleanup.

Remove this artificial restriction and expose them publicly via static
methods (this is equivalent to expose a singleton, but less verbose).
2022-10-02 17:57:35 +02:00
Romain Vimont 7505f7117e Fix typo in logs 2022-09-27 14:12:37 +02:00
SeungHoon Han 949b64dff2 Add fallback to get DisplayInfo
PR #3416 <https://github.com/Genymobile/scrcpy/pull/3416>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-25 16:22:51 +02:00
Romain Vimont 00e9e69c2a Use /dev/null instead of closing fds
Some adb commands do not like when stdin, stdout or stderr are closed
(they hang forever). Open /dev/null for each.
2022-09-25 15:42:33 +02:00
Romain Vimont 4a5cdcd390 Extract $BUILD_TOOLS_DIR
In the script to build without gradle, the build-tools full path is used
at several places. Use a separate variable for readability.
2022-09-25 14:26:07 +02:00
Romain Vimont e5e210506f Add scrcpy-console.desktop
Add a launcher which opens a terminal, and keep it open in case of
errors (so that the user has time to read error messages).

The behavior is the same as scrcpy-console.bat on Windows.

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
2022-09-09 19:06:29 +02:00
Chih-Hsuan Yen a2a22f497f Use shell environment to execute launcher
Make Exec= compatible with $PATH configured in .bashrc or .zshrc…

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
Refs #296 <https://github.com/Genymobile/scrcpy/pull/296#discussion_r224987002>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-09 19:06:29 +02:00
Addison SnellingandChih-Hsuan Yen 51a1762cbd Add desktop entry file for Linux app launchers
Refs <https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html>

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
Replaces PR #296 <https://github.com/Genymobile/scrcpy/pull/296>
Fixes #295 <https://github.com/Genymobile/scrcpy/issues/295>
Fixes #748 <https://github.com/Genymobile/scrcpy/issues/748>
Fixes #1636 <https://github.com/Genymobile/scrcpy/issues/1636>

Co-authored-by: Chih-Hsuan Yen <yan12125@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-09-09 19:06:29 +02:00
Romain Vimont c1ec1d1023 Replace hardcoded 'share/' by datadir variable
Meson defines a variable for the data directory.

PR #3351 <https://github.com/Genymobile/scrcpy/pull/3351>
2022-09-09 19:06:29 +02:00
Romain Vimont 0a0a446ea6 Upgrade Android SDK to 33 2022-09-02 14:42:37 +02:00
Romain Vimont fccfc43b9e Upgrade gradle build tools to 7.2.2
Plugin version 7.2.2.
Gradle version 7.3.3.

Refs: <https://developer.android.com/studio/releases/gradle-plugin#updating-gradle>
2022-09-02 14:40:16 +02:00
Romain Vimont 121bb71dfe Move from jcenter() to mavenCentral()
Refs <https://developer.android.com/studio/build/jcenter-migration>
Refs <https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/>
2022-09-02 14:31:15 +02:00
Simon Chan 57056d078d Use precise scrolling values
Since SDL 2.0.18, the amount scrolled horizontally or vertically is
exposed as a float (between 0 and 1). Forward a precise value to the
Android device when possible.

Refs <https://wiki.libsdl.org/SDL_MouseWheelEvent>
Fixes #3363 <https://github.com/Genymobile/scrcpy/issues/3363>
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-08-28 15:23:08 +02:00
Romain Vimont 1f138aef41 Add conversion from float to fixed-point i16
To encode float values between -1 and 1.

PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 1ab6c19486 Add unit test for float encoding
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont fd3483c837 Extract conversion from float to u16 fixed-point
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 041cdf6cf5 Rename buffer_util.h to binary.h
It will allow to expose more binary util functions not related to
buffers.

PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 136ab8c199 Add unit test for float decoding
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 3848ce86f1 Extract conversion from u16 fixed-point to float
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 5b8e9aa0e9 Move toUnsigned() to a Binary util class
PR #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
2022-08-28 15:23:08 +02:00
Romain Vimont 3a66b5fd01 Remove deprecated meson.source_root()
This method is deprecated since Meson 0.56.0:
<https://mesonbuild.com/Release-notes-for-0-56-0.html#mesonbuild_root-and-mesonsource_root-are-deprecated>

We could replace it with meson.project_source_root(), but this would
make Meson 0.56 or above mandatory. Since the path in always computed
from the server/ directory, just add '..' to reference the root project
directory.

Refs c456e38264
2022-08-28 15:16:31 +02:00
Derek Wu 9c1722f428 Use DisplayManagerGlobal instance
Use the client instance to communicate with the DisplayManager server.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2022-08-19 18:03:38 +02:00
Romain Vimont d19606eb0c Rename net_listen() parameter
For consistency with net_accept(), which necessarily uses a server
socket, name the net_listen() parameter "server_socket".
2022-08-17 16:40:45 +02:00
Romain Vimont d23b3e88a4 Replace '%g' by '%f' as printf format
For some reason, '%g' does not work correctly with MinGW.

Refs #3369 <https://github.com/Genymobile/scrcpy/pull/3369>
PR #3399 <https://github.com/Genymobile/scrcpy/pull/3399>
2022-08-03 23:25:09 +02:00
Romain Vimont a47848f304 Detect Windows using _WIN32 in network util
For consistency, always use _WIN32 instead of a mix of __WINDOWS__ and
_WIN32.
2022-07-27 14:54:27 +02:00
Romain Vimont db8c1ce8e1 Fix protocol documentation in comments
Flags were in the correct order in the schema, but their description
were reversed.
2022-07-20 11:41:04 +02:00
Romain Vimont 4aeb78ece2 Add missing allocation failure check 2022-07-19 12:17:02 +02:00
Romain Vimont 396e4bd925 Add missing LOG_OOM() on malloc failure 2022-07-19 12:15:06 +02:00
Romain Vimont 7f2f5950f2 Remove useless dependencies reference
There is no libs/ directory with local jar files.
2022-06-20 21:23:05 +02:00
Romain Vimont af4b7855e1 Remove unused stream.h
The file was not removed by 7dec225ceb.
2022-06-09 15:02:42 +02:00
Romain Vimont b1d8c72780 Rename function to simplify
For consistency with sc_adb_parse_device(), do not include "from_output"
in the function name.
2022-06-09 15:02:42 +02:00
Romain Vimont 55e65fa270 Add missing return 0 in tests 2022-06-09 15:02:42 +02:00
Romain Vimont 69fb5f6ee1 Fix function declarations
Add missing void in function parameters list.
2022-06-09 15:02:42 +02:00
132 changed files with 5385 additions and 1628 deletions
+5 -5
View File
@@ -260,7 +260,7 @@ set ANDROID_SDK_ROOT=%LOCALAPPDATA%\Android\sdk
Then, build:
```bash
meson x --buildtype=release --strip -Db_lto=true
meson setup x --buildtype=release --strip -Db_lto=true
ninja -Cx # DO NOT RUN AS ROOT
```
@@ -272,16 +272,16 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v1.24`][direct-scrcpy-server]
<sub>SHA-256: `ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056`</sub>
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:
```bash
meson x --buildtype=release --strip -Db_lto=true \
meson setup x --buildtype=release --strip -Db_lto=true \
-Dprebuilt_server=/path/to/scrcpy-server
ninja -Cx # DO NOT RUN AS ROOT
```
+2 -2
View File
@@ -277,7 +277,7 @@ The server is pushed to the device by the client on startup.
To debug it, enable the server debugger during configuration:
```bash
meson x -Dserver_debugger=true
meson setup x -Dserver_debugger=true
# or, if x is already configured
meson configure x -Dserver_debugger=true
```
@@ -286,7 +286,7 @@ If your device runs Android 8 or below, set the `server_debugger_method` to
`old` in addition:
```bash
meson x -Dserver_debugger=true -Dserver_debugger_method=old
meson setup x -Dserver_debugger=true -Dserver_debugger_method=old
# or, if x is already configured
meson configure x -Dserver_debugger=true -Dserver_debugger_method=old
```
+38 -27
View File
@@ -1,4 +1,4 @@
# scrcpy (v1.24)
# scrcpy (v1.25)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
@@ -106,10 +106,10 @@ process][BUILD_simple]).
For Windows, a prebuilt archive with all the dependencies (including `adb`) is
available:
- [`scrcpy-win64-v1.24.zip`][direct-win64]
<sub>SHA-256: `6ccb64cba0a3e75715e85a188daeb4f306a1985f8ce123eba92ba74fc9b27367`</sub>
- [`scrcpy-win64-v1.25.zip`][direct-win64]
<sub>SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028`</sub>
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-win64-v1.24.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip
It is also available in [Chocolatey]:
@@ -186,7 +186,7 @@ increase performance.
To limit both the width and height to some value (e.g. 1024):
```bash
scrcpy --max-size 1024
scrcpy --max-size=1024
scrcpy -m 1024 # short version
```
@@ -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
```
@@ -208,7 +208,7 @@ scrcpy -b 2M # short version
The capture frame rate can be limited:
```bash
scrcpy --max-fps 15
scrcpy --max-fps=15
```
This is officially supported since Android 10, but may work on earlier versions.
@@ -229,7 +229,7 @@ The device screen may be cropped to mirror only part of the screen.
This is useful, for example, to mirror only one eye of the Oculus Go:
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
```
If `--max-size` is also specified, resizing is applied after cropping.
@@ -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
@@ -275,14 +286,14 @@ scrcpy --encoder _
It is possible to record the screen while mirroring:
```bash
scrcpy --record file.mp4
scrcpy --record=file.mp4
scrcpy -r file.mkv
```
To disable mirroring while recording:
```bash
scrcpy --no-display --record file.mp4
scrcpy --no-display --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
@@ -395,8 +406,8 @@ address), connect the device over USB, then run:
scrcpy --tcpip # without arguments
```
It will automatically find the device IP address, enable TCP/IP mode, then
connect to the device before starting.
It will automatically find the device IP address and adb port, enable TCP/IP
mode if necessary, then connect to the device before starting.
##### Manual
@@ -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
```
@@ -443,7 +454,7 @@ scrcpy -b2M -m800 # short version
If several devices are listed in `adb devices`, you can specify the _serial_:
```bash
scrcpy --serial 0123456789abcdef
scrcpy --serial=0123456789abcdef
scrcpy -s 0123456789abcdef # short version
```
@@ -453,7 +464,7 @@ The serial may also be provided via the environment variable `ANDROID_SERIAL`
If the device is connected over TCP/IP:
```bash
scrcpy --serial 192.168.0.1:5555
scrcpy --serial=192.168.0.1:5555
scrcpy -s 192.168.0.1:5555 # short version
```
@@ -606,7 +617,7 @@ scrcpy --force-adb-forward
Like for wireless connections, it may be useful to reduce quality:
```
scrcpy -b2M -m800 --max-fps 15
scrcpy -b2M -m800 --max-fps=15
```
### Window configuration
@@ -616,7 +627,7 @@ scrcpy -b2M -m800 --max-fps 15
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
scrcpy --window-title='My device'
```
#### Position and size
@@ -624,7 +635,7 @@ scrcpy --window-title 'My device'
The initial window position and size may be specified:
```bash
scrcpy --window-x 100 --window-y 100 --window-width 800 --window-height 600
scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
```
#### Borderless
@@ -659,7 +670,7 @@ Fullscreen can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>.
The window may be rotated:
```bash
scrcpy --rotation 1
scrcpy --rotation=1
```
Possible values:
@@ -701,7 +712,7 @@ If several displays are available, it is possible to select the display to
mirror:
```bash
scrcpy --display 1
scrcpy --display=1
```
The list of display ids can be retrieved by:
+26 -6
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
@@ -93,7 +109,12 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur"))
return
;;
-b|--bitrate \
-s|--serial)
# Use 'adb devices' to list serial numbers
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return
;;
-b|--video-bit-rate \
|--codec-options \
|--crop \
|--display \
@@ -103,7 +124,6 @@ _scrcpy() {
|-m|--max-size \
|-p|--port \
|--push-target \
|-s|--serial \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \
+13
View File
@@ -0,0 +1,13 @@
[Desktop Entry]
Name=scrcpy (console)
GenericName=Android Remote Control
Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized.
Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
Icon=scrcpy
Terminal=true
Type=Application
Categories=Utility;RemoteAccess;
StartupNotify=false
+13
View File
@@ -0,0 +1,13 @@
[Desktop Entry]
Name=scrcpy
GenericName=Android Remote Control
Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized.
Exec=/bin/sh -c '"$SHELL" -i -c scrcpy'
Icon=scrcpy
Terminal=false
Type=Application
Categories=Utility;RemoteAccess;
StartupNotify=false
+12 -4
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]'
@@ -47,7 +52,7 @@ arguments=(
'--record-format=[Force recording format]:format:(mp4 mkv)'
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
{-t,--show-touches}'[Show physical touches]'
@@ -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]'
+36 -20
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',
@@ -37,6 +41,7 @@ src = [
'src/util/net_intr.c',
'src/util/process.c',
'src/util/process_intr.c',
'src/util/rand.c',
'src/util/strbuf.c',
'src/util/str.c',
'src/util/term.c',
@@ -97,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'),
]
@@ -127,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: [
@@ -170,6 +171,8 @@ check_functions = [
'strdup',
'asprintf',
'vasprintf',
'nrand48',
'jrand48',
]
foreach f : check_functions
@@ -197,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'))
@@ -223,14 +222,26 @@ executable('scrcpy', src,
install: true,
c_args: [])
# <https://mesonbuild.com/Builtin-options.html#directories>
datadir = get_option('datadir') # by default 'share'
install_man('scrcpy.1')
install_data('data/icon.png',
rename: 'scrcpy.png',
install_dir: 'share/icons/hicolor/256x256/apps')
install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps'))
install_data('data/zsh-completion/_scrcpy',
install_dir: 'share/zsh/site-functions')
install_dir: join_paths(datadir, 'zsh/site-functions'))
install_data('data/bash-completion/scrcpy',
install_dir: 'share/bash-completion/completions')
install_dir: join_paths(datadir, 'bash-completion/completions'))
# Desktop entry file for application launchers
if host_machine.system() == 'linux'
# Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop)
install_data('data/scrcpy.desktop',
install_dir: join_paths(datadir, 'applications'))
install_data('data/scrcpy-console.desktop',
install_dir: join_paths(datadir, 'applications'))
endif
### TESTS
@@ -245,8 +256,12 @@ if get_option('buildtype') == 'debug'
'src/util/str.c',
'src/util/strbuf.c',
]],
['test_buffer_util', [
'tests/test_buffer_util.c',
['test_binary', [
'tests/test_binary.c',
]],
['test_bytebuf', [
'tests/test_bytebuf.c',
'src/util/bytebuf.c',
]],
['test_cbuf', [
'tests/test_cbuf.c',
@@ -255,6 +270,7 @@ if get_option('buildtype') == 'debug'
'tests/test_cli.c',
'src/cli.c',
'src/options.c',
'src/util/log.c',
'src/util/net.c',
'src/util/str.c',
'src/util/strbuf.c',
+3 -3
View File
@@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=platform-tools-33.0.1
DEP_DIR=platform-tools-33.0.3
FILENAME=platform-tools_r33.0.1-windows.zip
SHA256SUM=c1f02d42ea24ef4ff2a405ae7370e764ef4546f9b3e4520f5571a00ed5012c42
FILENAME=platform-tools_r33.0.3-windows.zip
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
if [[ -d "$DEP_DIR" ]]
then
-45
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"
-36
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.0.1
DEP_DIR=ffmpeg-win64-$VERSION
FILENAME=ffmpeg-$VERSION-full_build-shared.7z
SHA256SUM=ded28435b6f04b74f5ef5a6a13761233bce9e8e9f8ecb0eabe936fd36a778b0c
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"
+30
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"
+3 -4
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
+3 -3
View File
@@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=SDL2-2.0.22
DEP_DIR=SDL2-2.26.1
FILENAME=SDL2-devel-2.0.22-mingw.tar.gz
SHA256SUM=0e91e35973366aa1e6f81ee368924d9b4f93f9da4d2f2a89ec80b06eadcf23d1
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
if [[ -d "$DEP_DIR" ]]
then
+1 -1
View File
@@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "1.24"
VALUE "ProductVersion", "1.25"
END
END
BLOCK "VarFileInfo"
+54 -21
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,12 +281,12 @@ 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).
If no destination address is provided, then scrcpy attempts to find the IP address of the current device (typically connected over USB), enables TCP/IP mode, then connects to this address before starting.
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
.TP
.B \-S, \-\-turn\-screen\-off
@@ -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.
+2 -1
View File
@@ -401,6 +401,7 @@ sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
#define BUFSIZE 65536
char *buf = malloc(BUFSIZE);
if (!buf) {
LOG_OOM();
return false;
}
@@ -710,5 +711,5 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
// It is parsed as a NUL-terminated string
buf[r] = '\0';
return sc_adb_parse_device_ip_from_output(buf);
return sc_adb_parse_device_ip(buf);
}
+1 -1
View File
@@ -199,7 +199,7 @@ sc_adb_parse_device_ip_from_line(char *line) {
}
char *
sc_adb_parse_device_ip_from_output(char *str) {
sc_adb_parse_device_ip(char *str) {
size_t idx_line = 0;
while (str[idx_line] != '\0') {
char *line = &str[idx_line];
+1 -1
View File
@@ -25,6 +25,6 @@ sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec);
* Warning: this function modifies the buffer for optimization purposes.
*/
char *
sc_adb_parse_device_ip_from_output(char *str);
sc_adb_parse_device_ip(char *str);
#endif
+13 -11
View File
@@ -7,8 +7,6 @@
#include "util/net_intr.h"
#include "util/process_intr.h"
#define SC_SOCKET_NAME "scrcpy"
static bool
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
@@ -17,10 +15,11 @@ listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
static bool
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!sc_adb_reverse(intr, serial, SC_SOCKET_NAME, port,
if (!sc_adb_reverse(intr, serial, device_socket_name, port,
SC_ADB_NO_STDOUT)) {
// the command itself failed, it will fail on any port
return false;
@@ -52,7 +51,7 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
}
// failure, disable tunnel and try another port
if (!sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
@@ -78,12 +77,13 @@ enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
static bool
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
struct sc_intr *intr, const char *serial,
const char *device_socket_name,
struct sc_port_range port_range) {
tunnel->forward = true;
uint16_t port = port_range.first;
for (;;) {
if (sc_adb_forward(intr, serial, port, SC_SOCKET_NAME,
if (sc_adb_forward(intr, serial, port, device_socket_name,
SC_ADB_NO_STDOUT)) {
// success
tunnel->local_port = port;
@@ -123,13 +123,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range,
bool force_adb_forward) {
const char *serial, const char *device_socket_name,
struct sc_port_range port_range, bool force_adb_forward) {
assert(!tunnel->enabled);
if (!force_adb_forward) {
// Attempt to use "adb reverse"
if (enable_tunnel_reverse_any_port(tunnel, intr, serial, port_range)) {
if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
device_socket_name, port_range)) {
return true;
}
@@ -139,12 +140,13 @@ sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
LOGW("'adb reverse' failed, fallback to 'adb forward'");
}
return enable_tunnel_forward_any_port(tunnel, intr, serial, port_range);
return enable_tunnel_forward_any_port(tunnel, intr, serial,
device_socket_name, port_range);
}
bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial) {
const char *serial, const char *device_socket_name) {
assert(tunnel->enabled);
bool ret;
@@ -152,7 +154,7 @@ sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
SC_ADB_NO_STDOUT);
} else {
ret = sc_adb_reverse_remove(intr, serial, SC_SOCKET_NAME,
ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
SC_ADB_NO_STDOUT);
assert(tunnel->server_socket != SC_SOCKET_NONE);
+3 -3
View File
@@ -34,14 +34,14 @@ sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
*/
bool
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial, struct sc_port_range port_range,
bool force_adb_forward);
const char *serial, const char *device_socket_name,
struct sc_port_range port_range, bool force_adb_forward);
/**
* Close the tunnel
*/
bool
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
const char *serial);
const char *serial, const char *device_socket_name);
#endif
+304
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
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
+221 -57
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;
+1 -1
View File
@@ -98,7 +98,7 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
sc_clock_estimate(clock, &clock->slope, &clock->offset);
#ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: %g * pts + %" PRItick,
LOGD("Clock estimation: %f * pts + %" PRItick,
clock->slope, clock->offset);
#endif
}
+44
View File
@@ -51,3 +51,47 @@ int vasprintf(char **strp, const char *fmt, va_list ap) {
return len;
}
#endif
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
#define SC_RAND48_C 0xB
static inline uint64_t rand_iter48(uint64_t x) {
assert((x & ~SC_RAND48_MASK) == 0);
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
}
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
uint64_t x = ((uint64_t) xsubi[0] << 32)
| ((uint64_t) xsubi[1] << 16)
| xsubi[2];
x = rand_iter48(x);
xsubi[0] = (x >> 32) & 0XFFFF;
xsubi[1] = (x >> 16) & 0XFFFF;
xsubi[2] = x & 0XFFFF;
return x;
}
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]) {
// range [0, 2^31)
return rand_iter48_xsubi(xsubi) >> 17;
}
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]) {
// range [-2^31, 2^31)
union {
uint32_t u;
int32_t i;
} v;
v.u = rand_iter48_xsubi(xsubi) >> 16;
return v.i;
}
#endif
#endif
+15
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
@@ -59,4 +66,12 @@ int asprintf(char **strp, const char *fmt, ...);
int vasprintf(char **strp, const char *fmt, va_list ap);
#endif
#ifndef HAVE_NRAND48
long nrand48(unsigned short xsubi[3]);
#endif
#ifndef HAVE_JRAND48
long jrand48(unsigned short xsubi[3]);
#endif
#endif
+40 -27
View File
@@ -5,7 +5,7 @@
#include <stdlib.h>
#include <string.h>
#include "util/buffer_util.h"
#include "util/binary.h"
#include "util/log.h"
#include "util/str.h"
@@ -37,7 +37,7 @@ static const char *const android_motionevent_action_labels[] = {
"move",
"cancel",
"outside",
"ponter-down",
"pointer-down",
"pointer-up",
"hover-move",
"scroll",
@@ -61,6 +61,22 @@ static const char *const copy_key_labels[] = {
"cut",
};
static inline const char *
get_well_known_pointer_id_name(uint64_t pointer_id) {
switch (pointer_id) {
case POINTER_ID_MOUSE:
return "mouse";
case POINTER_ID_GENERIC_FINGER:
return "finger";
case POINTER_ID_VIRTUAL_MOUSE:
return "vmouse";
case POINTER_ID_VIRTUAL_FINGER:
return "vfinger";
default:
return NULL;
}
}
static void
write_position(uint8_t *buf, const struct sc_position *position) {
sc_write32be(&buf[0], position->point.x);
@@ -78,16 +94,6 @@ write_string(const char *utf8, size_t max_len, unsigned char *buf) {
return 4 + len;
}
static uint16_t
to_fixed_point_16(float f) {
assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
u = 0xffff;
}
return (uint16_t) u;
}
size_t
sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
buf[0] = msg->type;
@@ -109,18 +115,21 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
sc_write64be(&buf[2], msg->inject_touch_event.pointer_id);
write_position(&buf[10], &msg->inject_touch_event.position);
uint16_t pressure =
to_fixed_point_16(msg->inject_touch_event.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);
sc_write32be(&buf[13],
(uint32_t) msg->inject_scroll_event.hscroll);
sc_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll);
sc_write32be(&buf[21], msg->inject_scroll_event.buttons);
return 25;
int16_t hscroll =
sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
int16_t vscroll =
sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
sc_write16be(&buf[13], (uint16_t) hscroll);
sc_write16be(&buf[15], (uint16_t) vscroll);
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);
return 21;
case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->inject_keycode.action;
return 2;
@@ -167,32 +176,36 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
int action = msg->inject_touch_event.action
& AMOTION_EVENT_ACTION_MASK;
uint64_t id = msg->inject_touch_event.pointer_id;
if (id == POINTER_ID_MOUSE || id == POINTER_ID_VIRTUAL_FINGER) {
const char *pointer_name = get_well_known_pointer_id_name(id);
if (pointer_name) {
// string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%g buttons=%06lx",
id == POINTER_ID_MOUSE ? "mouse" : "vfinger",
" 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=%g 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;
}
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%" PRIi32
" vscroll=%" PRIi32 " buttons=%06lx",
LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f"
" vscroll=%f buttons=%06lx",
msg->inject_scroll_event.position.point.x,
msg->inject_scroll_event.position.point.y,
msg->inject_scroll_event.hscroll,
+8 -3
View File
@@ -18,7 +18,11 @@
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
#define POINTER_ID_MOUSE UINT64_C(-1)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2)
#define POINTER_ID_GENERIC_FINGER UINT64_C(-2)
// Used for injecting an additional virtual pointer for pinch-to-zoom
#define POINTER_ID_VIRTUAL_MOUSE UINT64_C(-3)
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-4)
enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
@@ -61,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;
@@ -68,8 +73,8 @@ struct sc_control_msg {
} inject_touch_event;
struct {
struct sc_position position;
int32_t hscroll;
int32_t vscroll;
float hscroll;
float vscroll;
enum android_motionevent_buttons buttons;
} inject_scroll_event;
struct {
+6 -6
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);
}
+1 -1
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
+26 -10
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 = {
+4 -1
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);
+118 -79
View File
@@ -6,8 +6,9 @@
#include "decoder.h"
#include "events.h"
#include "packet_merger.h"
#include "recorder.h"
#include "util/buffer_util.h"
#include "util/binary.h"
#include "util/log.h"
#define SC_PACKET_HEADER_SIZE 12
@@ -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
@@ -37,8 +74,8 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// CK...... ........ ........ ........ ........ ........ ........ ........
// ^^<------------------------------------------------------------------->
// || PTS
// | `- config packet
// `-- key frame
// | `- key frame
// `-- config packet
uint8_t header[SC_PACKET_HEADER_SIZE];
ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
@@ -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,52 +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) {
size_t offset;
if (demuxer->pending) {
offset = demuxer->pending->size;
if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM();
return false;
}
} else {
offset = 0;
demuxer->pending = av_packet_alloc();
if (!demuxer->pending) {
LOG_OOM();
return false;
}
if (av_new_packet(demuxer->pending, packet->size)) {
LOG_OOM();
av_packet_free(&demuxer->pending);
return false;
}
}
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
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;
}
@@ -160,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;
}
@@ -169,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) {
@@ -221,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;
@@ -263,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;
+5 -8
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
+1 -1
View File
@@ -4,7 +4,7 @@
#include <stdlib.h>
#include <string.h>
#include "util/buffer_util.h"
#include "util/binary.h"
#include "util/log.h"
ssize_t
+7 -5
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)
+1 -1
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;
}
+4 -2
View File
@@ -353,18 +353,20 @@ struct sc_mouse_click_event {
struct sc_position position;
enum sc_action action;
enum sc_mouse_button button;
uint64_t pointer_id;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
};
struct sc_mouse_scroll_event {
struct sc_position position;
int32_t hscroll;
int32_t vscroll;
float hscroll;
float vscroll;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
};
struct sc_mouse_motion_event {
struct sc_position position;
uint64_t pointer_id;
int32_t xrel;
int32_t yrel;
uint8_t buttons_state; // bitwise-OR of sc_mouse_button values
+15 -3
View File
@@ -335,8 +335,11 @@ simulate_virtual_finger(struct sc_input_manager *im,
msg.inject_touch_event.action = action;
msg.inject_touch_event.position.screen_size = im->screen->frame_size;
msg.inject_touch_event.position.point = point;
msg.inject_touch_event.pointer_id = POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pointer_id =
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)) {
@@ -564,6 +567,8 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
event->x,
event->y),
},
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state =
@@ -687,6 +692,8 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
},
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.pointer_id = im->forward_all_clicks ? POINTER_ID_MOUSE
: POINTER_ID_GENERIC_FINGER,
.buttons_state =
sc_mouse_buttons_state_from_sdl(sdl_buttons_state,
im->forward_all_clicks),
@@ -747,8 +754,13 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
.point = sc_screen_convert_window_to_frame_coords(im->screen,
mouse_x, mouse_y),
},
.hscroll = event->x,
.vscroll = event->y,
#if SDL_VERSION_ATLEAST(2, 0, 18)
.hscroll = CLAMP(event->preciseX, -1.0f, 1.0f),
.vscroll = CLAMP(event->preciseY, -1.0f, 1.0f),
#else
.hscroll = CLAMP(event->x, -1, 1),
.vscroll = CLAMP(event->y, -1, 1),
#endif
.buttons_state =
sc_mouse_buttons_state_from_sdl(buttons, im->forward_all_clicks),
};
+60 -5
View File
@@ -15,11 +15,17 @@
#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(int argc, char *argv[]) {
#ifdef __WINDOWS__
main_scrcpy(int argc, char *argv[]) {
#ifdef _WIN32
// disable buffering, we want logs immediately
// even line buffering (setvbuf() with mode _IOLBF) is not sufficient
setbuf(stdout, NULL);
@@ -65,10 +71,12 @@ main(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);
@@ -76,7 +84,54 @@ main(int argc, char *argv[]) {
enum scrcpy_exit_code ret = scrcpy(&args.opts);
#endif
avformat_network_deinit(); // ignore failure
return ret;
}
int
main(int argc, char *argv[]) {
#ifndef _WIN32
return main_scrcpy(argc, argv);
#else
(void) argc;
(void) argv;
int wargc;
wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc);
if (!wargv) {
LOG_OOM();
return SCRCPY_EXIT_FAILURE;
}
char **argv_utf8 = malloc((wargc + 1) * sizeof(*argv_utf8));
if (!argv_utf8) {
LOG_OOM();
LocalFree(wargv);
return SCRCPY_EXIT_FAILURE;
}
argv_utf8[wargc] = NULL;
for (int i = 0; i < wargc; ++i) {
argv_utf8[i] = sc_str_from_wchars(wargv[i]);
if (!argv_utf8[i]) {
LOG_OOM();
for (int j = 0; j < i; ++j) {
free(argv_utf8[j]);
}
LocalFree(wargv);
free(argv_utf8);
return SCRCPY_EXIT_FAILURE;
}
}
LocalFree(wargv);
int ret = main_scrcpy(wargc, argv_utf8);
for (int i = 0; i < wargc; ++i) {
free(argv_utf8[i]);
}
free(argv_utf8);
return ret;
#endif
}
+3 -2
View File
@@ -69,7 +69,7 @@ sc_mouse_processor_process_mouse_motion(struct sc_mouse_processor *mp,
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = AMOTION_EVENT_ACTION_MOVE,
.pointer_id = POINTER_ID_MOUSE,
.pointer_id = event->pointer_id,
.position = event->position,
.pressure = 1.f,
.buttons = convert_mouse_buttons(event->buttons_state),
@@ -90,9 +90,10 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.type = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
.inject_touch_event = {
.action = convert_mouse_action(event->action),
.pointer_id = POINTER_ID_MOUSE,
.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),
},
};
+12 -3
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,
};
+19 -3
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
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
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
+7 -7
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);
}
+7 -7
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
+620 -221
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);
}
+46 -12
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);
+180 -90
View File
@@ -13,6 +13,7 @@
# include <windows.h>
#endif
#include "audio_player.h"
#include "controller.h"
#include "decoder.h"
#include "demuxer.h"
@@ -32,6 +33,7 @@
#include "util/acksync.h"
#include "util/log.h"
#include "util/net.h"
#include "util/rand.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
@@ -39,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;
@@ -154,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;
@@ -175,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;
@@ -194,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
@@ -244,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
@@ -252,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
@@ -265,6 +275,15 @@ 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_scid() {
struct sc_rand rand;
sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side
return sc_rand_u32(&rand) & 0x7FFFFFFF;
}
enum scrcpy_exit_code
scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy;
@@ -283,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;
@@ -298,25 +319,34 @@ scrcpy(struct scrcpy_options *options) {
struct sc_acksync *acksync = NULL;
uint32_t scid = scrcpy_generate_scid();
struct sc_server_params params = {
.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,
@@ -325,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 = {
@@ -342,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);
@@ -357,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;
@@ -383,41 +432,55 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
struct sc_decoder *dec = NULL;
bool needs_decoder = options->display;
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_decoder |= !!options->v4l2_device;
needs_video_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
sc_decoder_init(&s->decoder);
dec = &s->decoder;
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;
@@ -608,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
@@ -618,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:
@@ -662,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);
}
@@ -673,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
@@ -693,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);
@@ -707,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);
}
@@ -716,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;
+8 -4
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");
+193 -56
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"
@@ -19,6 +20,9 @@
#define SC_SERVER_PATH_DEFAULT PREFIX "/share/scrcpy/" SC_SERVER_FILENAME
#define SC_DEVICE_SERVER_PATH "/data/local/tmp/scrcpy-server.jar"
#define SC_ADB_PORT_DEFAULT 5555
#define SC_SOCKET_NAME_PREFIX "scrcpy_"
static char *
get_server_path(void) {
#ifdef __WINDOWS__
@@ -67,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);
}
@@ -91,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
@@ -152,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) {
@@ -195,9 +221,25 @@ execute_server(struct sc_server *server,
cmd[count++] = p; \
}
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);
}
@@ -227,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");
@@ -252,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
@@ -362,9 +416,11 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
}
server->serial = NULL;
server->device_socket_name = NULL;
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);
@@ -393,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;
}
@@ -409,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);
@@ -419,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);
@@ -445,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
@@ -461,7 +538,8 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
}
// we don't need the adb tunnel anymore
sc_adb_tunnel_close(tunnel, &server->intr, serial);
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
// The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, video_socket, info);
@@ -470,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;
@@ -484,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");
@@ -492,7 +578,8 @@ fail:
if (tunnel->enabled) {
// Always leave this function with tunnel disabled
sc_adb_tunnel_close(tunnel, &server->intr, serial);
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
}
return false;
@@ -513,27 +600,36 @@ sc_server_on_terminated(void *userdata) {
LOGD("Server terminated");
}
static bool
is_tcpip_mode_enabled(struct sc_server *server, const char *serial) {
static uint16_t
get_adb_tcp_port(struct sc_server *server, const char *serial) {
struct sc_intr *intr = &server->intr;
char *current_port =
sc_adb_getprop(intr, serial, "service.adb.tcp.port", SC_ADB_SILENT);
if (!current_port) {
return false;
return 0;
}
// Is the device is listening on TCP on port 5555?
bool enabled = !strcmp("5555", current_port);
long value;
bool ok = sc_str_parse_integer(current_port, &value);
free(current_port);
return enabled;
if (!ok) {
return 0;
}
if (value < 0 || value > 0xFFFF) {
return 0;
}
return value;
}
static bool
wait_tcpip_mode_enabled(struct sc_server *server, const char *serial,
unsigned attempts, sc_tick delay) {
if (is_tcpip_mode_enabled(server, serial)) {
LOGI("TCP/IP mode enabled");
uint16_t expected_port, unsigned attempts,
sc_tick delay) {
uint16_t adb_port = get_adb_tcp_port(server, serial);
if (adb_port == expected_port) {
return true;
}
@@ -547,28 +643,23 @@ wait_tcpip_mode_enabled(struct sc_server *server, const char *serial,
return false;
}
if (is_tcpip_mode_enabled(server, serial)) {
LOGI("TCP/IP mode enabled");
adb_port = get_adb_tcp_port(server, serial);
if (adb_port == expected_port) {
return true;
}
} while (--attempts);
return false;
}
char *
append_port_5555(const char *ip) {
size_t len = strlen(ip);
// sizeof counts the final '\0'
char *ip_port = malloc(len + sizeof(":5555"));
if (!ip_port) {
static char *
append_port(const char *ip, uint16_t port) {
char *ip_port;
int ret = asprintf(&ip_port, "%s:%" PRIu16, ip, port);
if (ret == -1) {
LOG_OOM();
return NULL;
}
memcpy(ip_port, ip, len);
memcpy(ip_port + len, ":5555", sizeof(":5555"));
return ip_port;
}
@@ -586,34 +677,36 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) {
return NULL;
}
char *ip_port = append_port_5555(ip);
free(ip);
if (!ip_port) {
return NULL;
}
uint16_t adb_port = get_adb_tcp_port(server, serial);
if (adb_port) {
LOGI("TCP/IP mode already enabled on port %" PRIu16, adb_port);
} else {
LOGI("Enabling TCP/IP mode on port " SC_STR(SC_ADB_PORT_DEFAULT) "...");
bool tcp_mode = is_tcpip_mode_enabled(server, serial);
if (!tcp_mode) {
bool ok = sc_adb_tcpip(intr, serial, 5555, SC_ADB_NO_STDOUT);
bool ok = sc_adb_tcpip(intr, serial, SC_ADB_PORT_DEFAULT,
SC_ADB_NO_STDOUT);
if (!ok) {
LOGE("Could not restart adbd in TCP/IP mode");
goto error;
free(ip);
return NULL;
}
unsigned attempts = 40;
sc_tick delay = SC_TICK_FROM_MS(250);
ok = wait_tcpip_mode_enabled(server, serial, attempts, delay);
ok = wait_tcpip_mode_enabled(server, serial, SC_ADB_PORT_DEFAULT,
attempts, delay);
if (!ok) {
goto error;
free(ip);
return NULL;
}
adb_port = SC_ADB_PORT_DEFAULT;
LOGI("TCP/IP mode enabled on port " SC_STR(SC_ADB_PORT_DEFAULT));
}
char *ip_port = append_port(ip, adb_port);
free(ip);
return ip_port;
error:
free(ip_port);
return NULL;
}
static bool
@@ -640,7 +733,8 @@ sc_server_configure_tcpip_known_address(struct sc_server *server,
const char *addr) {
// Append ":5555" if no port is present
bool contains_port = strchr(addr, ':');
char *ip_port = contains_port ? strdup(addr) : append_port_5555(addr);
char *ip_port = contains_port ? strdup(addr)
: append_port(addr, SC_ADB_PORT_DEFAULT);
if (!ip_port) {
LOG_OOM();
return false;
@@ -658,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;
}
@@ -760,8 +859,32 @@ run_server(void *data) {
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->scid);
if (r == -1) {
LOG_OOM();
goto error_connection_failed;
}
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
assert(server->device_socket_name);
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
params->port_range, params->force_adb_forward);
server->device_socket_name, params->port_range,
params->force_adb_forward);
if (!ok) {
goto error_connection_failed;
}
@@ -769,7 +892,8 @@ run_server(void *data) {
// server will connect to our server socket
sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) {
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed;
}
@@ -781,7 +905,8 @@ run_server(void *data) {
if (!ok) {
sc_process_terminate(pid);
sc_process_wait(pid, true); // ignore exit code
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial);
sc_adb_tunnel_close(&server->tunnel, &server->intr, serial,
server->device_socket_name);
goto error_connection_failed;
}
@@ -809,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);
@@ -861,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);
}
@@ -870,11 +1003,15 @@ 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);
}
free(server->serial);
free(server->device_socket_name);
sc_server_params_destroy(&server->params);
sc_intr_destroy(&server->intr);
sc_cond_destroy(&server->cond_stopped);
+18 -3
View File
@@ -22,20 +22,27 @@ struct sc_server_info {
};
struct sc_server_params {
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;
@@ -48,12 +55,15 @@ struct sc_server_params {
bool select_tcpip;
bool cleanup;
bool power_on;
bool list_encoders;
bool list_displays;
};
struct sc_server {
// The internal allocated strings are copies owned by the server
struct sc_server_params params;
char *serial;
char *device_socket_name;
sc_thread thread;
struct sc_server_info info; // initialized once connected
@@ -66,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;
@@ -105,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);
-51
View File
@@ -1,51 +0,0 @@
#ifndef STREAM_H
#define STREAM_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include "trait/packet_sink.h"
#include "util/net.h"
#include "util/thread.h"
#define STREAM_MAX_SINKS 2
struct stream {
sc_socket socket;
sc_thread thread;
struct sc_packet_sink *sinks[STREAM_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 stream_callbacks *cbs;
void *cbs_userdata;
};
struct stream_callbacks {
void (*on_eos)(struct stream *stream, void *userdata);
};
void
stream_init(struct stream *stream, sc_socket socket,
const struct stream_callbacks *cbs, void *cbs_userdata);
void
stream_add_sink(struct stream *stream, struct sc_packet_sink *sink);
bool
stream_start(struct stream *stream);
void
stream_join(struct stream *stream);
#endif
+19 -5
View File
@@ -92,8 +92,14 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
close(in[0]);
}
close(in[1]);
} else {
int devnull = open("/dev/null", O_RDONLY | O_CREAT, 0666);
if (devnull != -1) {
dup2(devnull, STDIN_FILENO);
} else {
LOGE("Could not open /dev/null for stdin");
}
}
// Do not close stdin in the child process, this makes adb fail on Linux
if (pout) {
if (out[1] != STDOUT_FILENO) {
@@ -102,8 +108,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
}
close(out[0]);
} else if (!inherit_stdout) {
// Close stdout in the child process
close(STDOUT_FILENO);
int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
if (devnull != -1) {
dup2(devnull, STDOUT_FILENO);
} else {
LOGE("Could not open /dev/null for stdout");
}
}
if (perr) {
@@ -113,8 +123,12 @@ sc_process_execute_p(const char *const argv[], sc_pid *pid, unsigned flags,
}
close(err[0]);
} else if (!inherit_stderr) {
// Close stderr in the child process
close(STDERR_FILENO);
int devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);
if (devnull != -1) {
dup2(devnull, STDERR_FILENO);
} else {
LOGE("Could not open /dev/null for stderr");
}
}
close(internal[0]);
+2 -1
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);
};
+11
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
+2 -2
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:
+5
View File
@@ -23,6 +23,11 @@ read_string(libusb_device_handle *handle, uint8_t desc_index) {
// When non-negative, 'result' contains the number of bytes written
char *s = malloc(result + 1);
if (!s) {
LOG_OOM();
return NULL;
}
memcpy(s, buffer, result);
s[result] = '\0';
return s;
+26
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
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
@@ -1,8 +1,9 @@
#ifndef SC_BUFFER_UTIL_H
#define SC_BUFFER_UTIL_H
#ifndef SC_BINARY_H
#define SC_BINARY_H
#include "common.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
@@ -43,4 +44,33 @@ sc_read64be(const uint8_t *buf) {
return ((uint64_t) msb << 32) | lsb;
}
/**
* Convert a float between 0 and 1 to an unsigned 16-bit fixed-point value
*/
static inline uint16_t
sc_float_to_u16fp(float f) {
assert(f >= 0.0f && f <= 1.0f);
uint32_t u = f * 0x1p16f; // 2^16
if (u >= 0xffff) {
assert(u == 0x10000); // for f == 1.0f
u = 0xffff;
}
return (uint16_t) u;
}
/**
* Convert a float between -1 and 1 to a signed 16-bit fixed-point value
*/
static inline int16_t
sc_float_to_i16fp(float f) {
assert(f >= -1.0f && f <= 1.0f);
int32_t i = f * 0x1p15f; // 2^15
assert(i >= -0x8000);
if (i >= 0x7fff) {
assert(i == 0x8000); // for f == 1.0f
i = 0x7fff;
}
return (int16_t) i;
}
#endif
+116
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
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
+45
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);
}
+3
View File
@@ -35,4 +35,7 @@ bool
sc_log_windows_error(const char *prefix, int error);
#endif
void
sc_log_configure();
#endif
+11 -12
View File
@@ -3,11 +3,10 @@
#include <assert.h>
#include <errno.h>
#include <stdio.h>
#include <SDL2/SDL_platform.h>
#include "log.h"
#ifdef __WINDOWS__
#ifdef _WIN32
# include <ws2tcpip.h>
typedef int socklen_t;
typedef SOCKET sc_raw_socket;
@@ -29,10 +28,10 @@
bool
net_init(void) {
#ifdef __WINDOWS__
#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;
}
@@ -42,14 +41,14 @@ net_init(void) {
void
net_cleanup(void) {
#ifdef __WINDOWS__
#ifdef _WIN32
WSACleanup();
#endif
}
static inline sc_socket
wrap(sc_raw_socket sock) {
#ifdef __WINDOWS__
#ifdef _WIN32
if (sock == INVALID_SOCKET) {
return SC_SOCKET_NONE;
}
@@ -72,7 +71,7 @@ wrap(sc_raw_socket sock) {
static inline sc_raw_socket
unwrap(sc_socket socket) {
#ifdef __WINDOWS__
#ifdef _WIN32
if (socket == SC_SOCKET_NONE) {
return INVALID_SOCKET;
}
@@ -160,8 +159,8 @@ net_connect(sc_socket socket, uint32_t addr, uint16_t port) {
}
bool
net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog) {
sc_raw_socket raw_sock = unwrap(socket);
net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog) {
sc_raw_socket raw_sock = unwrap(server_socket);
int reuse = 1;
if (setsockopt(raw_sock, SOL_SOCKET, SO_REUSEADDR, (const void *) &reuse,
@@ -248,7 +247,7 @@ net_interrupt(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket);
#ifdef __WINDOWS__
#ifdef _WIN32
if (!atomic_flag_test_and_set(&socket->closed)) {
return !closesocket(raw_sock);
}
@@ -262,7 +261,7 @@ bool
net_close(sc_socket socket) {
sc_raw_socket raw_sock = unwrap(socket);
#ifdef __WINDOWS__
#ifdef _WIN32
bool ret = true;
if (!atomic_flag_test_and_set(&socket->closed)) {
ret = !closesocket(raw_sock);
+3 -4
View File
@@ -5,9 +5,8 @@
#include <stdbool.h>
#include <stdint.h>
#include <SDL2/SDL_platform.h>
#ifdef __WINDOWS__
#ifdef _WIN32
# include <winsock2.h>
# include <stdatomic.h>
@@ -17,7 +16,7 @@
atomic_flag closed;
} *sc_socket;
#else // not __WINDOWS__
#else // not _WIN32
# include <sys/socket.h>
# define SC_SOCKET_NONE -1
@@ -40,7 +39,7 @@ bool
net_connect(sc_socket socket, uint32_t addr, uint16_t port);
bool
net_listen(sc_socket socket, uint32_t addr, uint16_t port, int backlog);
net_listen(sc_socket server_socket, uint32_t addr, uint16_t port, int backlog);
sc_socket
net_accept(sc_socket server_socket);
+3 -3
View File
@@ -15,14 +15,14 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
}
bool
net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr,
uint16_t port, int backlog) {
if (!sc_intr_set_socket(intr, socket)) {
if (!sc_intr_set_socket(intr, server_socket)) {
// Already interrupted
return false;
}
bool ret = net_listen(socket, addr, port, backlog);
bool ret = net_listen(server_socket, addr, port, backlog);
sc_intr_set_socket(intr, SC_SOCKET_NONE);
return ret;
+1 -1
View File
@@ -11,7 +11,7 @@ net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
uint16_t port);
bool
net_listen_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr,
net_listen_intr(struct sc_intr *intr, sc_socket server_socket, uint32_t addr,
uint16_t port, int backlog);
sc_socket
+24
View File
@@ -0,0 +1,24 @@
#include "rand.h"
#include <stdlib.h>
#include "tick.h"
void sc_rand_init(struct sc_rand *rand) {
sc_tick seed = sc_tick_now(); // microsecond precision
rand->xsubi[0] = (seed >> 32) & 0xFFFF;
rand->xsubi[1] = (seed >> 16) & 0xFFFF;
rand->xsubi[2] = seed & 0xFFFF;
}
uint32_t sc_rand_u32(struct sc_rand *rand) {
// jrand returns a value in range [-2^31, 2^31]
// conversion from signed to unsigned is well-defined to wrap-around
return jrand48(rand->xsubi);
}
uint64_t sc_rand_u64(struct sc_rand *rand) {
uint32_t msb = sc_rand_u32(rand);
uint32_t lsb = sc_rand_u32(rand);
return ((uint64_t) msb << 32) | lsb;
}
+16
View File
@@ -0,0 +1,16 @@
#ifndef SC_RAND_H
#define SC_RAND_H
#include "common.h"
#include <inttypes.h>
struct sc_rand {
unsigned short xsubi[3];
};
void sc_rand_init(struct sc_rand *rand);
uint32_t sc_rand_u32(struct sc_rand *rand);
uint64_t sc_rand_u64(struct sc_rand *rand);
#endif
+4
View File
@@ -6,6 +6,10 @@
#include <stdbool.h>
#include <stddef.h>
/* Stringify a numeric value */
#define SC_STR(s) SC_XSTR(s)
#define SC_XSTR(s) #s
/**
* Like strncpy(), except:
* - it copies at most n-1 chars
+1
View File
@@ -1,6 +1,7 @@
#include "thread.h"
#include <assert.h>
#include <string.h>
#include <SDL2/SDL_thread.h>
#include "log.h"
+1
View File
@@ -6,6 +6,7 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
// Adapted from vlc_vector:
// <https://code.videolan.org/videolan/vlc/-/blob/0857947abaed9c89810cd96353aaa1b7e6ba3b0d/include/vlc_vector.h>
+6 -3
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
+26 -24
View File
@@ -5,7 +5,7 @@
#include "adb/adb_device.h"
#include "adb/adb_parser.h"
static void test_adb_devices() {
static void test_adb_devices(void) {
char output[] =
"List of devices attached\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@@ -31,7 +31,7 @@ static void test_adb_devices() {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_cr() {
static void test_adb_devices_cr(void) {
char output[] =
"List of devices attached\r\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@@ -57,7 +57,7 @@ static void test_adb_devices_cr() {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_daemon_start() {
static void test_adb_devices_daemon_start(void) {
char output[] =
"* daemon not running; starting now at tcp:5037\n"
"* daemon started successfully\n"
@@ -78,7 +78,7 @@ static void test_adb_devices_daemon_start() {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_daemon_start_mixed() {
static void test_adb_devices_daemon_start_mixed(void) {
char output[] =
"List of devices attached\n"
"adb server version (41) doesn't match this client (39); killing...\n"
@@ -105,7 +105,7 @@ static void test_adb_devices_daemon_start_mixed() {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_without_eol() {
static void test_adb_devices_without_eol(void) {
char output[] =
"List of devices attached\n"
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
@@ -124,7 +124,7 @@ static void test_adb_devices_without_eol() {
sc_adb_devices_destroy(&vec);
}
static void test_adb_devices_without_header() {
static void test_adb_devices_without_header(void) {
char output[] =
"0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
"device:MyDevice transport_id:1\n";
@@ -134,7 +134,7 @@ static void test_adb_devices_without_header() {
assert(!ok);
}
static void test_adb_devices_corrupted() {
static void test_adb_devices_corrupted(void) {
char output[] =
"List of devices attached\n"
"corrupted_garbage\n";
@@ -145,7 +145,7 @@ static void test_adb_devices_corrupted() {
assert(vec.size == 0);
}
static void test_adb_devices_spaces() {
static void test_adb_devices_spaces(void) {
char output[] =
"List of devices attached\n"
"0123456789abcdef unauthorized usb:1-4 transport_id:3\n";
@@ -163,81 +163,81 @@ static void test_adb_devices_spaces() {
sc_adb_devices_destroy(&vec);
}
static void test_get_ip_single_line() {
static void test_get_ip_single_line(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34\r\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.12.34"));
free(ip);
}
static void test_get_ip_single_line_without_eol() {
static void test_get_ip_single_line_without_eol(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.12.34"));
free(ip);
}
static void test_get_ip_single_line_with_trailing_space() {
static void test_get_ip_single_line_with_trailing_space(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.12.34 \n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.12.34"));
free(ip);
}
static void test_get_ip_multiline_first_ok() {
static void test_get_ip_multiline_first_ok(void) {
char ip_route[] = "192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.2\r\n"
"10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.2\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.1.2"));
free(ip);
}
static void test_get_ip_multiline_second_ok() {
static void test_get_ip_multiline_second_ok(void) {
char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.3\r\n"
"192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.3\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.1.3"));
free(ip);
}
static void test_get_ip_no_wlan() {
static void test_get_ip_no_wlan(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34\r\r\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip);
}
static void test_get_ip_no_wlan_without_eol() {
static void test_get_ip_no_wlan_without_eol(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip);
}
static void test_get_ip_truncated() {
static void test_get_ip_truncated(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"\n";
char *ip = sc_adb_parse_device_ip_from_output(ip_route);
char *ip = sc_adb_parse_device_ip(ip_route);
assert(!ip);
}
@@ -262,4 +262,6 @@ int main(int argc, char *argv[]) {
test_get_ip_no_wlan();
test_get_ip_no_wlan_without_eol();
test_get_ip_truncated();
return 0;
}
+114
View File
@@ -0,0 +1,114 @@
#include "common.h"
#include <assert.h>
#include "util/binary.h"
static void test_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
sc_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
sc_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
sc_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = sc_read16be(buf);
assert(val == 0xABCD);
}
static void test_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = sc_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = sc_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
static void test_float_to_u16fp(void) {
assert(sc_float_to_u16fp(0.0f) == 0);
assert(sc_float_to_u16fp(0.03125f) == 0x800);
assert(sc_float_to_u16fp(0.0625f) == 0x1000);
assert(sc_float_to_u16fp(0.125f) == 0x2000);
assert(sc_float_to_u16fp(0.25f) == 0x4000);
assert(sc_float_to_u16fp(0.5f) == 0x8000);
assert(sc_float_to_u16fp(0.75f) == 0xc000);
assert(sc_float_to_u16fp(1.0f) == 0xffff);
}
static void test_float_to_i16fp(void) {
assert(sc_float_to_i16fp(0.0f) == 0);
assert(sc_float_to_i16fp(0.03125f) == 0x400);
assert(sc_float_to_i16fp(0.0625f) == 0x800);
assert(sc_float_to_i16fp(0.125f) == 0x1000);
assert(sc_float_to_i16fp(0.25f) == 0x2000);
assert(sc_float_to_i16fp(0.5f) == 0x4000);
assert(sc_float_to_i16fp(0.75f) == 0x6000);
assert(sc_float_to_i16fp(1.0f) == 0x7fff);
assert(sc_float_to_i16fp(-0.03125f) == -0x400);
assert(sc_float_to_i16fp(-0.0625f) == -0x800);
assert(sc_float_to_i16fp(-0.125f) == -0x1000);
assert(sc_float_to_i16fp(-0.25f) == -0x2000);
assert(sc_float_to_i16fp(-0.5f) == -0x4000);
assert(sc_float_to_i16fp(-0.75f) == -0x6000);
assert(sc_float_to_i16fp(-1.0f) == -0x8000);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_write16be();
test_write32be();
test_write64be();
test_read16be();
test_read32be();
test_read64be();
test_float_to_u16fp();
test_float_to_i16fp();
return 0;
}
-81
View File
@@ -1,81 +0,0 @@
#include "common.h"
#include <assert.h>
#include "util/buffer_util.h"
static void test_buffer_write16be(void) {
uint16_t val = 0xABCD;
uint8_t buf[2];
sc_write16be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
}
static void test_buffer_write32be(void) {
uint32_t val = 0xABCD1234;
uint8_t buf[4];
sc_write32be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
}
static void test_buffer_write64be(void) {
uint64_t val = 0xABCD1234567890EF;
uint8_t buf[8];
sc_write64be(buf, val);
assert(buf[0] == 0xAB);
assert(buf[1] == 0xCD);
assert(buf[2] == 0x12);
assert(buf[3] == 0x34);
assert(buf[4] == 0x56);
assert(buf[5] == 0x78);
assert(buf[6] == 0x90);
assert(buf[7] == 0xEF);
}
static void test_buffer_read16be(void) {
uint8_t buf[2] = {0xAB, 0xCD};
uint16_t val = sc_read16be(buf);
assert(val == 0xABCD);
}
static void test_buffer_read32be(void) {
uint8_t buf[4] = {0xAB, 0xCD, 0x12, 0x34};
uint32_t val = sc_read32be(buf);
assert(val == 0xABCD1234);
}
static void test_buffer_read64be(void) {
uint8_t buf[8] = {0xAB, 0xCD, 0x12, 0x34,
0x56, 0x78, 0x90, 0xEF};
uint64_t val = sc_read64be(buf);
assert(val == 0xABCD1234567890EF);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_buffer_write16be();
test_buffer_write32be();
test_buffer_write64be();
test_buffer_read16be();
test_buffer_read32be();
test_buffer_read64be();
return 0;
}
+154
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;
}
+2 -2
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);
+7 -5
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)));
}
@@ -132,14 +134,14 @@ static void test_serialize_inject_scroll_event(void) {
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 25);
assert(size == 21);
const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026
0x04, 0x38, 0x07, 0x80, // 1080 1920
0x00, 0x00, 0x00, 0x01, // 1
0xFF, 0xFF, 0xFF, 0xFF, // -1
0x7F, 0xFF, // 1 (float encoded as i16)
0x80, 0x00, // -1 (float encoded as i16)
0x00, 0x00, 0x00, 0x01, // 1
};
assert(!memcmp(buf, expected, sizeof(expected)));
+3 -3
View File
@@ -4,10 +4,10 @@ buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.3'
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
@@ -17,7 +17,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:deprecation"
+3 -7
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_sdl2 = 'SDL2-2.0.22/i686-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = prebuilt_libusb_root + '/MinGW-Win32'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32'
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
+3 -7
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.0.1'
prebuilt_sdl2 = 'SDL2-2.0.22/x86_64-w64-mingw32'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = prebuilt_libusb_root + '/MinGW-x64'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64'
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+3 -3
View File
@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.24/scrcpy-server-v1.24
PREBUILT_SERVER_SHA256=ae74a81ea79c0dc7250e586627c278c0a9a8c5de46c9fb5c38c167fb1a36f056
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb
echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
@@ -12,7 +12,7 @@ echo "$PREBUILT_SERVER_SHA256 scrcpy-server" | sha256sum --check
echo "[scrcpy] Building client..."
rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
meson setup "$BUILDDIR" --buildtype=release --strip -Db_lto=true \
-Dprebuilt_server=scrcpy-server
cd "$BUILDDIR"
ninja
+1 -1
View File
@@ -1,5 +1,5 @@
project('scrcpy', 'c',
version: '1.24',
version: '1.25',
meson_version: '>= 0.48',
default_options: [
'c_std=c11',
+39 -45
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 \
@@ -24,13 +24,13 @@ SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32
WIN64_TARGET_DIR := scrcpy-win64
VERSION := $(shell git describe --tags --always)
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION)
@@ -53,39 +53,33 @@ clean:
test:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson "$(TEST_BUILD_DIR)" -Db_sanitize=address )
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
$(GRADLE) -p server check
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
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 "$(WIN32_BUILD_DIR)" \
meson setup "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcompile_server=false \
-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 "$(WIN64_BUILD_DIR)" \
meson setup "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcompile_server=false \
@@ -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/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.0.22/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/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/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,21 +113,21 @@ 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.0.1/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.0.1/bin/swscale-6.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-33.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.0.22/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/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/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
zip -r "../$(WIN32_TARGET)" .
cd "$(DIST)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
zip -r "../$(WIN64_TARGET)" .
cd "$(DIST)"; \
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
+6 -6
View File
@@ -1,13 +1,14 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 31
namespace 'com.genymobile.scrcpy'
compileSdkVersion 33
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 31
versionCode 12400
versionName "1.24"
targetSdkVersion 33
versionCode 12500
versionName "1.25"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
@@ -19,8 +20,7 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.13.1'
testImplementation 'junit:junit:4.13.2'
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"
+20 -15
View File
@@ -12,25 +12,29 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.24
SCRCPY_VERSION_NAME=1.25
PLATFORM=${ANDROID_PLATFORM:-31}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
GEN_DIR="$BUILD_DIR/gen"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar"
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
rm -rf "$CLASSES_DIR" "$GEN_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR"
mkdir -p "$GEN_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
<< EOF cat > "$GEN_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy;
public final class BuildConfig {
@@ -41,14 +45,15 @@ EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/view/IRotationWatcher.aidl
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
@@ -59,8 +64,7 @@ cd "$CLASSES_DIR"
if [[ $PLATFORM -lt 31 ]]
then
# use dx
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
@@ -69,10 +73,10 @@ then
echo "Archiving..."
cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes
rm -rf classes.dex
else
# use d8
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \
"$BUILD_TOOLS_DIR/d8" --classpath "$ANDROID_JAR" \
--output "$BUILD_DIR/classes.zip" \
android/view/*.class \
android/content/*.class \
@@ -81,7 +85,8 @@ else
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"
rm -rf classes
fi
rm -rf "$GEN_DIR" "$CLASSES_DIR"
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"
+2 -2
View File
@@ -13,8 +13,8 @@ if prebuilt_server == ''
install_dir: 'share/scrcpy')
else
if not prebuilt_server.startswith('/')
# relative path needs some trick
prebuilt_server = meson.source_root() + '/' + prebuilt_server
# prebuilt server path is relative to the root scrcpy directory
prebuilt_server = '../' + prebuilt_server
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
+1 -1
View File
@@ -1,2 +1,2 @@
<!-- not a real Android application, it is run by app_process manually -->
<manifest package="com.genymobile.scrcpy"/>
<manifest />
@@ -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;
}
}
@@ -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
}
}
}
@@ -0,0 +1,38 @@
package com.genymobile.scrcpy;
public final class Binary {
private Binary() {
// not instantiable
}
public static int toUnsigned(short value) {
return value & 0xffff;
}
public static int toUnsigned(byte value) {
return value & 0xff;
}
/**
* Convert unsigned 16-bit fixed-point to a float between 0 and 1
*
* @param value encoded value
* @return Float value between 0 and 1
*/
public static float u16FixedPointToFloat(short value) {
int unsignedShort = Binary.toUnsigned(value);
// 0x1p16f is 2^16 as float
return unsignedShort == 0xffff ? 1f : (unsignedShort / 0x1p16f);
}
/**
* Convert signed 16-bit fixed-point to a float between -1 and 1
*
* @param value encoded value
* @return Float value between -1 and 1
*/
public static float i16FixedPointToFloat(short value) {
// 0x1p15f is 2^15 as float
return value == 0x7fff ? 1f : (value / 0x1p15f);
}
}
@@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
@@ -141,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) {
@@ -164,12 +162,10 @@ public final class CleanUp {
Config config = Config.fromBase64(args[0]);
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
Settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
@@ -177,7 +173,7 @@ public final class CleanUp {
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
Settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
} catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
}
@@ -0,0 +1,17 @@
package com.genymobile.scrcpy;
public interface Codec {
enum Type {
VIDEO,
AUDIO,
}
Type getType();
int getId();
String getName();
String getMimeType();
}
@@ -4,8 +4,8 @@ import java.util.ArrayList;
import java.util.List;
public class CodecOption {
private String key;
private Object value;
private final String key;
private final Object value;
public CodecOption(String key, Object value) {
this.key = key;

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