Compare commits

..

119 Commits

Author SHA1 Message Date
Romain Vimont
a373a47544 Document audio player
Add some high-level documentation on the audio player implementation.
2023-03-09 19:24:37 +01:00
Romain Vimont
426d510ef0 Decrease recorder thread priority
Recording is background task, writing the packets to a file is not
urgent.
2023-03-09 19:21:34 +01:00
Romain Vimont
f71419fd55 Increase audio thread priority
The audio demuxer thread is the one filling the audio buffer read by the
SDL audio thread. It is time critical to avoid buffer underflow.
2023-03-09 19:21:33 +01:00
Romain Vimont
4e85c48592 Add thread priority API
Expose an API to change the priority of the current thread.
2023-03-09 19:21:33 +01:00
Romain Vimont
ff82852f28 Print info logs to stdout
All server logs were printed to stdout, while all client logs were
printed to stderr.

Instead, use stderr for warnings and errors, stdout for the others:
 - stdout: verbose, debug, info
 - stderr: warn, error
2023-03-09 19:21:33 +01:00
Romain Vimont
9f8f312c50 Print server logs and newline in one call
System.out.println() first prints the message, then the new line.
Between these two calls, the client might print a message, breaking
formatting.

Instead, call System.out.print() with '\n' appended to the message.
2023-03-09 19:21:33 +01:00
Romain Vimont
68d3a327a1 Warn on ignored audio options
For raw audio codec, some audio options are ignored.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:33 +01:00
Romain Vimont
b08deb5224 Add --audio-codec=raw option
Add support for raw (PCM S16 LE) audio codec (a raw decoder is included
in FFmpeg).

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:33 +01:00
Romain Vimont
2611f7f0ca Add raw audio recorder
Add an alternative AudioRecorder to stream raw packets without encoding.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:33 +01:00
Romain Vimont
81838f1678 Extract async processor interface
On the server side, several components are started, stopped and joined.
Extract an interface to handle them generically.

This will help to support both encoded and raw audio stream, because
they will be two different concrete components, but implementing the
same interface.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:32 +01:00
Romain Vimont
5fff56ee6f Extract audio capture
The audio capture was implemented in AudioEncoder.

In order to reuse it without encoding, extract it to a separate class.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:32 +01:00
Romain Vimont
7c0338c3e6 Stop on decoder frame push error
On push, frame sinks report downstream errors to stop upstream
components. Do not ignore the error.
2023-03-09 19:21:32 +01:00
Romain Vimont
cdd0cdbceb Add --audio-buffer
Expose an option to add a buffering delay (in milliseconds) before
playing audio.

This is similar to the options --display-buffer and --v4l2-buffer for
video frames.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:32 +01:00
Romain Vimont
aaea8e106f 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:31 +01:00
Romain Vimont
cc8023fcf8 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.

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

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-03-09 19:21:31 +01:00
Romain Vimont
a8bfb1c3ef Optionally do not delay the first frame
A delay buffer delayed all the frames except the first one, to open the
scrcpy window immediately and get a picture.

Make this feature optional, so that the delay buffer might also be used
for audio (especially for simulating a high delay for debugging).

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:31 +01:00
Romain Vimont
0f957645cc Accept clock estimation with a single point
If there is only one point, assume the slope is 1.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:30 +01:00
Romain Vimont
55158932d2 Remove anonymous struct in delay buffer
For clarity, the fields used only when a delay was set were wrapped in
an anonymous structure.

Now that the delay buffer has been extracted to a separate component,
the delay is necessarily set (it may not be 0), so the fields are always
used.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:30 +01:00
Romain Vimont
a5ebd16d5c Use delay buffer as a frame source/sink
The components needing delayed frames (sc_screen and sc_v4l2_sink)
managed a sc_video_buffer instance, which itself embedded a
sc_frame_buffer instance (to keep only the most recent frame).

In theory, these components should not be aware of delaying: they should
just receive AVFrames later, and only handle a sc_frame_buffer.

Therefore, refactor sc_delay_buffer as a frame source (it consumes)
frames) and a frame sink (it produces frames, after some delay), and
plug an instance in the pipeline only when a delay is requested.

This also removes the need for a specific sc_video_buffer.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:30 +01:00
Romain Vimont
2dae48bc13 Use frame source trait in decoder 2023-03-09 19:21:30 +01:00
Romain Vimont
c39e6dda3e Introduce frame source trait
There was a frame sink trait, implemented by components able to receive
AVFrames, but each frame source had to manually send frame to sinks.

In order to mutualise sink management, add a frame sink trait.
2023-03-09 19:21:30 +01:00
Romain Vimont
974230fa95 Use packet source trait in demuxer 2023-03-09 19:21:30 +01:00
Romain Vimont
4132389686 Introduce packet source trait
There was a packet sink trait, implemented by components able to
receive AVPackets, but each packet source had to manually send packets
to sinks.

In order to mutualise sink management, add a packet source trait.
2023-03-09 19:21:30 +01:00
Romain Vimont
34dae7f836 Extract sc_delay_buffer
A video buffer had 2 responsibilities:
 - handle the frame delaying mechanism (queuing packets and pushing them
   after the expected delay);
 - keep only the most recent frame (using a sc_frame_buffer).

In order to be able to reuse only the frame delaying mechanism, extract
it to a separate component, sc_delay_buffer.
2023-03-09 19:21:30 +01:00
Romain Vimont
828b6e786f Fix buffering pts conversion
The mistake had no effect, because tick is also internally expressed in
microseconds.
2023-03-09 19:21:29 +01:00
Romain Vimont
34b778d423 Report video buffer downstream errors
Make the video buffer stop if its consumer could not receive a frame.
2023-03-09 19:21:29 +01:00
Romain Vimont
18b60c9641 Stop the video buffer on error
If an error occurs from the video buffer thread (typically an
out-of-memory error), then stop.
2023-03-09 19:21:28 +01:00
Romain Vimont
2b95b5630f Fix possible race condition on video_buffer end
The video_buffer thread clears the queue once it is stopped, but new
frames might still be pushed asynchronously.

To avoid the problem, do not push any frame once the video_buffer is
stopped.
2023-03-09 19:21:28 +01:00
Romain Vimont
0e1b173213 Remove sc_queue
All uses have been replaced by VecDeque.
2023-03-09 19:21:28 +01:00
Romain Vimont
a540301e3c Remove cbuf
All uses have been replaced by VecDeque.
2023-03-09 19:21:28 +01:00
Romain Vimont
29946bccc0 Use VecDeque in aoa_hid
Replace cbuf by VecDeque in aoa_hid
2023-03-09 19:21:27 +01:00
Romain Vimont
c20a53a17d Use VecDeque in file_pusher
Replace cbuf by VecDeque in file_pusher.

As a side-effect, the new implementation does not limit the queue to an
arbitrary value.
2023-03-09 19:21:27 +01:00
Romain Vimont
d2d69ddfcb Use VecDeque in controller
Replace cbuf by VecDeque in controller.
2023-03-09 19:21:27 +01:00
Romain Vimont
5a41b710f1 Use VecDeque in video_buffer
The packets queued for buffering were wrapped in a dynamically allocated
structure with a "next" field.

To avoid this additional layer of allocation and indirection, use a
VecDeque.
2023-03-09 19:21:27 +01:00
Romain Vimont
d8a48e871c Use VecDeque in recorder
The packets queued for recording were wrapped in a dynamically allocated
structure with a "next" field.

To avoid this additional layer of allocation and indirection, use a
VecDeque.
2023-03-09 19:21:27 +01:00
Romain Vimont
2615143711 Introduce VecDeque
Introduce a double-ended queue implemented with a growable ring buffer.

Inspired from the Rust VecDeque type:
<https://doc.rust-lang.org/std/collections/struct.VecDeque.html>
2023-03-09 19:21:26 +01:00
Romain Vimont
931bc84548 Add sc_allocarray() util
Add a function to allocate an array, which fails safely in the case
where the multiplication would overflow.
2023-03-09 19:21:26 +01:00
Romain Vimont
31c763fc62 Use reallocarray() in sc_vector
This fails safely in case of overflow.
2023-03-09 19:21:26 +01:00
Romain Vimont
20eeab921a Add compat for reallocarray()
This function fails safely in the case where the multiplication would
overflow.
2023-03-09 19:21:25 +01:00
Romain Vimont
93de9739c7 Call avcodec_receive_frame() in a loop
Since in scrcpy a video packet passed to avcodec_send_packet() is always
a complete video frame, it is sufficient to call avcodec_receive_frame()
exactly once.

In practice, it also works for audio packets: the decoder produces
exactly 1 frame for 1 input packet.

In theory, it is an implementation detail though, so
avcodec_receive_frame() should be called in a loop.
2023-03-09 19:21:25 +01:00
Romain Vimont
ce924d8f05 Add --require-audio
By default, scrcpy mirrors only the video when audio capture fails on
the device. Add an option to force scrcpy to fail if audio is enabled
but does not work.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:25 +01:00
Simon Chan
47f67412e4 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.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-09 19:21:24 +01:00
Romain Vimont
51016950cb 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 writes 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 a lock held, and update its
knowledge of the safe empty remaining space.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:24 +01:00
Romain Vimont
7203e58ebf Introduce bytebuf util
Add a ring-buffer for bytes. It will be useful for audio buffering.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:23 +01:00
Romain Vimont
25f213e4a6 Pass AVCodecContext to frame sinks
Frame consumers may need details about the frame format.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:22 +01:00
Romain Vimont
cb25bed2f5 Add an audio decoder
PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:22 +01:00
Romain Vimont
d4407f1289 Give a name to decoder instances
This will be useful in logs.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:22 +01:00
Romain Vimont
1daf8e8611 Rename decoder to video_decoder
This prepares the introduction of audio_decoder.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:22 +01:00
Romain Vimont
4c24ea50ed Log display sizes in display list
This is more convenient than just the display id alone.
2023-03-09 19:21:22 +01:00
Romain Vimont
eb39365737 Add --list-displays
Add an option to list the device displays properly.
2023-03-09 19:21:21 +01:00
Romain Vimont
46169618a5 Move log message helpers to LogUtils
This class will also contain other log helpers.
2023-03-09 19:21:21 +01:00
Romain Vimont
1a3c61a77b Quit on audio configuration failure
When audio capture fails on the device, scrcpy continues 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:21 +01:00
Romain Vimont
fe9166a88c Add --list-encoders
Add an option to list the device encoders properly.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:21 +01:00
Romain Vimont
e4eae49eed 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:21 +01:00
Romain Vimont
0b24cb930c Add --audio-encoder
Similar to --video-encoder, but for audio.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:21 +01:00
Romain Vimont
8edb1c8eef Extract unknown encoder error message
This will allow to reuse the same code for audio encoder selection.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
775789e50b Add --audio-codec-options
Similar to --video-codec-options, but for audio.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
b3641dc025 Extract application of codec options
This will allow to reuse the same code for audio codec options.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
5c24f32651 Add support for AAC audio codec
Add option --audio-codec=aac.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
7b0977c7d9 Add --audio-codec
Introduce the selection mechanism. Alternative codecs will be added
later.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
b992191572 Add --audio-bit-rate
Add an option to configure the audio bit-rate.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
a6de3e67b2 Disable MethodLength checkstyle on createOptions()
This method will grow as needed to initialize options.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:20 +01:00
Romain Vimont
94a14e7fd0 Rename --encoder to --video-encoder
This prepares the introduction of --audio-encoder.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
bf3b1c70df Rename --codec-options to --video-codec-options
This prepares the introduction of --audio-codec-options.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
4950d5c846 Rename --bit-rate to --video-bit-rate
This prepares the introduction of --audio-bit-rate.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
3f98478ad7 Rename --codec to --video-codec
This prepares the introduction of --audio-codec.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
4e7bc84a4e 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
ef2bb6787d 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
7daebaadae Disable audio before Android 11
The permission "android.permission.RECORD_AUDIO" has been added for
shell in Android 11.

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

Refs <4feeee8891%5E%21/>
PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
2bc9b687db 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
0a1451b301 Add audio recording support
Make the recorder accept two input sources (video and audio), and mux
them into a single file.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
d499e96d0e Rename video-specific variables in recorder
This paves the way to add audio-specific variables.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
ef22893b7c 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
fd655b0b79 Add an audio demuxer
Add a demuxer which will read the stream from the audio socket.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:19 +01:00
Romain Vimont
ddef08a0f7 Force --no-audio if no display and no recording
The client does not use the audio stream if there is no display and no
recording (i.e. only V4L2), so disable audio so that the device does not
attempt to capture it.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:18 +01:00
Romain Vimont
5be8cb79c4 Give a name to demuxer instances
This will be useful in logs.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:18 +01:00
Romain Vimont
0bb7f08d95 Rename demuxer to video_demuxer
There will be another demuxer instance for audio.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:18 +01:00
Romain Vimont
aff7b7f26a 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:18 +01:00
Romain Vimont
160de07e8a Use a streamer to send the audio stream
Send each encoded audio packet using a streamer.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:18 +01:00
Romain Vimont
805d85dc3b Encode recorded audio on the device
For now, the encoded packets are just logged into the console.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:17 +01:00
Romain Vimont
29c04995b4 Make streamer more generic
Expose a method to write a packet from raw metadata (without
BufferInfo).
2023-03-09 19:21:17 +01:00
Simon Chan
03a553f590 Capture device audio
Create an AudioRecorder to capture the audio source REMOTE_SUBMIX.

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

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-09 19:21:17 +01:00
Simon Chan
e2aa3cca52 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.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-09 19:21:17 +01:00
Simon Chan
de1dfae169 Add --no-audio option
Audio will be enabled by default (when supported). Add an option to
disable it.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-09 19:21:17 +01:00
Romain Vimont
b6a8b6f91a Use FakeContext for Application instance
This will expose the correct package name and UID to the application
context.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:17 +01:00
Romain Vimont
2346148827 Use shell package name for workarounds
For consistency.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:17 +01:00
Romain Vimont
27918a501c 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.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:17 +01:00
Romain Vimont
631801ce3e Use PACKAGE_NAME from FakeContext
Remove duplicated constant.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:16 +01:00
Romain Vimont
884d0ec246 Use AttributionSource from FakeContext
FakeContext already provides an AttributeSource instance.

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

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-03-09 19:21:16 +01:00
Simon Chan
db0c5bc066 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.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-09 19:21:16 +01:00
Romain Vimont
681fde542d Improve error message for unknown encoder
The provided encoder name depends on the selected codec. Improve the
error message and the suggestions.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:16 +01:00
Romain Vimont
fb12118190 Rename "codec" variable to "mediaCodec"
This will allow to use "codec" for the Codec type.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:16 +01:00
Romain Vimont
a1d84709ca Make streamer independent of codec type
Rename VideoStreamer to Streamer, and extract a Codec interface which
will also support audio codecs.

PR #3757 <https://github.com/Genymobile/scrcpy/pull/3757>
2023-03-09 19:21:16 +01:00
Romain Vimont
77277238b3 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-03-09 19:21:16 +01:00
Romain Vimont
b9ee7c5852 Move screen encoder initialization
This prepares further refactors.
2023-03-09 19:21:16 +01:00
Romain Vimont
ec6641b519 Write streamer header from ScreenEncoder
The screen encoder is responsible for writing data to the video
streamer.
2023-03-09 19:21:15 +01:00
Romain Vimont
8fb9c28c59 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-03-09 19:21:15 +01:00
Romain Vimont
836f0042af 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-03-09 19:21:15 +01:00
Romain Vimont
245cf146c5 Reorder initialization
Initialize components in the pipeline order: demuxer first, decoder and
recorder second.
2023-03-09 19:21:15 +01:00
Romain Vimont
adaf02957e Refactor recorder logic
Process the initial config packet (necessary to write the header)
separately.
2023-03-09 19:21:15 +01:00
Romain Vimont
2238a64c2f Move last packet recording
Write the last packet at the end.
2023-03-09 19:21:15 +01:00
Romain Vimont
6a19814779 Add start() function for recorder
For consistency with the other components, do not start the internal
thread from an init() function.
2023-03-09 19:21:15 +01:00
Romain Vimont
a2ff5750e7 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-03-09 19:21:14 +01:00
Romain Vimont
f87ae8e0e7 Inline packet_sink impl in recorder
Remove useless wrappers.
2023-03-09 19:21:14 +01:00
Romain Vimont
528841f294 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 (spoiler: one for video, one
for audio), then the mutex and condvar would still be initialized only
once.
2023-03-09 19:21:14 +01:00
Romain Vimont
2517c9c31e 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-03-09 19:21:14 +01:00
Romain Vimont
0704441b16 Move previous packet to a local variable
It is only used from run_recorder().
2023-03-09 19:21:14 +01:00
Romain Vimont
f98dc5525d Move pts_origin to a local variable
It is only used from run_recorder().
2023-03-09 19:21:14 +01:00
Romain Vimont
778c7c0a88 Change PTS origin type from uint64_t to int64_t
It is initialized from AVPacket.pts, which is an int64_t.
2023-03-09 19:21:14 +01:00
Romain Vimont
932e82e89b Fix --encoder documentation
Mention that it depends on the codec provided by --codec (which is not
necessarily H264 anymore).
2023-03-09 19:21:13 +01:00
Romain Vimont
4d719da424 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-03-09 19:21:13 +01:00
Romain Vimont
7d571e502a Fix --no-clipboard-autosync bash completion
Fix typo.
2023-03-09 19:21:13 +01:00
Romain Vimont
982c292794 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-03-09 19:21:13 +01:00
Romain Vimont
f5447f4391 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-03-09 19:21:12 +01:00
Romain Vimont
123a4d9575 Move FFmpeg callback initialization
Configure FFmpeg log redirection on start from a log helper.
2023-03-09 19:21:12 +01:00
Romain Vimont
c91e56ac1f Upgrade FFmpeg custom builds for Windows
Use a build which includes the pcm_s16le decoder, to support RAW audio.

Refs <https://github.com/rom1v/scrcpy-deps/commits/6.0-scrcpy-2>
2023-03-09 19:21:12 +01:00
Romain Vimont
d391de17c9 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-03-09 19:21:12 +01:00
Romain Vimont
06ba2e9402 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-03-09 19:21:12 +01:00
Romain Vimont
428cbd13ec Simplify libusb prebuilt scripts
In theory, include/ might be slightly different for win32 and win64
builds. Use each one separately to simplify.
2023-03-09 19:21:12 +01:00
89 changed files with 2758 additions and 3271 deletions

View File

@@ -2,16 +2,57 @@
Here are the instructions to build _scrcpy_ (client and server).
If you just want to build and install the latest release, follow the simplified
process described in [doc/linux.md](linux.md).
## Simple
If you just want to install the latest release from `master`, follow this
simplified process.
First, you need to install the required packages:
```bash
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
```
Then clone the repo and execute the installation script
([source](install_release.sh)):
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
./install_release.sh
```
When a new release is out, update the repo and reinstall:
```bash
git pull
./install_release.sh
```
To uninstall:
```bash
sudo ninja -Cbuild-auto uninstall
```
## Branches
There are two main branches:
- `master`: contains the latest release. It is the home page of the project on
GitHub.
- `dev`: the current development branch. Every commit present in `dev` will be
in the next release.
### `master`
The `master` branch concerns the latest release, and is the home page of the
project on GitHub.
### `dev`
`dev` is the current development branch. Every commit present in `dev` will be
in the next release.
If you want to contribute code, please base your commits on the latest `dev`
branch.
@@ -28,8 +69,6 @@ the following files to a directory accessible from your `PATH`:
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
It is also available in scrcpy releases.
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
[adb]: https://developer.android.com/studio/command-line/adb.html
@@ -233,10 +272,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server
- [`scrcpy-server-v2.0`][direct-scrcpy-server]
<sub>SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2`</sub>
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0
[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:
@@ -275,8 +314,7 @@ This installs several files:
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
You can then run `scrcpy`.
You can then [run](README.md#run) `scrcpy`.
### Uninstall

143
FAQ.md
View File

@@ -7,7 +7,7 @@ Here are the common reported problems and their status.
If you encounter any error, the first step is to upgrade to the latest version.
## `adb` and USB issues
## `adb` issues
`scrcpy` execute `adb` commands to initialize the connection with the device. If
`adb` fails, then scrcpy will not work.
@@ -133,21 +133,6 @@ Try with another USB cable or plug it into another USB port. See [#281] and
[#283]: https://github.com/Genymobile/scrcpy/issues/283
## HID/OTG issues on Windows
On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in:
> ERROR: Could not find any USB device
(or if only unrelated USB devices are detected), there might be drivers issues.
Please read [#3654], in particular [this comment][#3654-comment1] and [the next
one][#3654-comment2].
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
## Control issues
@@ -168,7 +153,8 @@ The default text injection method is [limited to ASCII characters][text-input].
A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37].
Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID).
Since scrcpy v1.20 on Linux, it is possible to simulate a [physical
keyboard][hid] (HID).
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
@@ -178,6 +164,32 @@ Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID).
## Client issues
### The quality is low
If the definition of your client window is smaller than that of your device
screen, then you might get poor quality, especially visible on text (see [#40]).
[#40]: https://github.com/Genymobile/scrcpy/issues/40
This problem should be fixed in scrcpy v1.22: **update to the latest version**.
On older versions, you must configure the [scaling behavior]:
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
> Override high DPI scaling behavior > Scaling performed by: _Application_.
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
Also, to improve downscaling quality, trilinear filtering is enabled
automatically if the renderer is OpenGL and if it supports mipmapping.
On Windows, you might want to force OpenGL to enable mipmapping:
```
scrcpy --render-driver=opengl
```
### Issue with Wayland
By default, SDL uses x11 on Linux. The [video driver] can be changed via the
@@ -212,15 +224,102 @@ As a workaround, [disable "Block compositing"][kwin].
### Exception
If you get any exception related to `MediaCodec`:
There may be many reasons. One common cause is that the hardware encoder of your
device is not able to encode at the given definition:
> ```
> ERROR: Exception on thread Thread[main,5,main]
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
> ...
> Exit due to uncaughtException in main thread:
> ERROR: Could not open video stream
> INFO: Initial texture: 1080x2336
> ```
or
> ```
> ERROR: Exception on thread Thread[main,5,main]
> java.lang.IllegalStateException
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
> ```
Just try with a lower definition:
```
ERROR: Exception on thread Thread[main,5,main]
java.lang.IllegalStateException
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
scrcpy -m 1920
scrcpy -m 1024
scrcpy -m 800
```
then try with another [encoder](doc/video.md#codec).
Since scrcpy v1.22, scrcpy automatically tries again with a lower definition
before failing. This behavior can be disabled with `--no-downsize-on-error`.
You could also try another [encoder](README.md#encoder).
If you encounter this exception on Android 12, then just upgrade to scrcpy >=
1.18 (see [#2129]):
```
> ERROR: Exception on thread Thread[main,5,main]
java.lang.AssertionError: java.lang.reflect.InvocationTargetException
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75)
...
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73)
... 7 more
Caused by: java.lang.IllegalArgumentException: displayToken must not be null
at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067)
at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147)
... 9 more
```
[#2129]: https://github.com/Genymobile/scrcpy/issues/2129
## Command line on Windows
Since v1.22, a "shortcut" has been added to directly open a terminal in the
scrcpy directory. Double-click on `open_a_terminal_here.bat`, then type your
command. For example:
```
scrcpy --record file.mkv
```
You could also open a terminal and go to the scrcpy folder manually:
1. Press <kbd>Windows</kbd>+<kbd>r</kbd>, this opens a dialog box.
2. Type `cmd` and press <kbd>Enter</kbd>, this opens a terminal.
3. Go to your _scrcpy_ directory, by typing (adapt the path):
```bat
cd C:\Users\user\Downloads\scrcpy-win64-xxx
```
and press <kbd>Enter</kbd>
4. Type your command. For example:
```bat
scrcpy --record file.mkv
```
If you plan to always use the same arguments, create a file `myscrcpy.bat`
(enable [show file extensions] to avoid confusion) in the `scrcpy` directory,
containing your command. For example:
```bat
scrcpy --prefer-text --turn-screen-off --stay-awake
```
Then just double-click on that file.
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
to add some arguments.
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
## Translations

View File

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

1216
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,9 @@ _scrcpy() {
local opts="
--always-on-top
--audio-bit-rate=
--audio-buffer=
--audio-codec=
--audio-codec-options=
--audio-encoder=
--audio-output-buffer=
-b --video-bit-rate=
--crop=
-d --select-usb
@@ -33,11 +31,10 @@ _scrcpy() {
--no-clipboard-autosync
--no-downsize-on-error
-n --no-control
-N --no-mirror
-N --no-display
--no-key-repeat
--no-mipmaps
--no-power-on
--no-video
--otg
-p --port=
--power-off-on-close
@@ -118,26 +115,20 @@ _scrcpy() {
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return
;;
--audio-bit-rate \
|--audio-buffer \
|-b|--video-bit-rate \
|--audio-codec-options \
|--audio-encoder \
|--audio-output-buffer \
-b|--video-bit-rate \
|--codec-options \
|--crop \
|--display \
|--display-buffer \
|--encoder \
|--max-fps \
|-m|--max-size \
|-p|--port \
|--push-target \
|--rotation \
|--tunnel-host \
|--tunnel-port \
|--v4l2-buffer \
|--v4l2-sink \
|--video-codec-options \
|--video-encoder \
|--tcpip \
|--window-*)
# Option accepting an argument, but nothing to auto-complete

View File

@@ -5,7 +5,7 @@ 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 Enter to quit...'"
Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
Icon=scrcpy
Terminal=true
Type=Application

View File

@@ -5,7 +5,7 @@ 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"
Exec=/bin/sh -c '"$SHELL" -i -c scrcpy'
Icon=scrcpy
Terminal=false
Type=Application

View File

@@ -10,11 +10,9 @@ local arguments
arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
'--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]'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'
@@ -39,11 +37,10 @@ arguments=(
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-mirror}'[Do not mirror device \(only when recording or V4L2 sink is enabled\)]'
{-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--power-off-on-close[Turn the device screen off when closing scrcpy]'

View File

@@ -14,7 +14,6 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/icon.c',
'src/file_pusher.c',
'src/fps_counter.c',
@@ -278,6 +277,10 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c',
'src/util/term.c',
]],
['test_clock', [
'tests/test_clock.c',
'src/clock.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',
@@ -307,8 +310,7 @@ if get_option('buildtype') == 'debug'
]
foreach t : tests
sources = t[1] + ['src/compat.c']
exe = executable(t[0], sources,
exe = executable(t[0], t[1],
include_directories: src_dir,
dependencies: dependencies,
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])

View File

@@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=platform-tools-34.0.1
DEP_DIR=platform-tools-33.0.3
FILENAME=platform-tools_r34.0.1-windows.zip
SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa
FILENAME=platform-tools_r33.0.3-windows.zip
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
if [[ -d "$DEP_DIR" ]]
then

View File

@@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.0-scrcpy-3
VERSION=6.0-scrcpy-2
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=36829d98ac4454d7092c72ddb92faa20b60450bc0fe8873076efb0858cdcbc2c
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14
if [[ -d "$DEP_DIR" ]]
then

View File

@@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=SDL2-2.26.4
DEP_DIR=SDL2-2.26.1
FILENAME=SDL2-devel-2.26.4-mingw.tar.gz
SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
if [[ -d "$DEP_DIR" ]]
then

View File

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

View File

@@ -23,24 +23,16 @@ Make scrcpy window always on top (above other windows).
.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 128K (128000).
Default is 196K (196000).
.TP
.BI "\-\-audio\-buffer ms
Configure the audio buffering delay (in milliseconds).
Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches).
Lower values decrease the latency, but increase the likelyhood of buffer underruns (causing audio glitches).
Default is 50.
.TP
.BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
Default is 5.
.TP
.BI "\-\-audio\-codec " name
Select an audio codec (opus, aac or raw).
@@ -183,10 +175,6 @@ It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR.
.TP
.B \-\-no\-audio
Disable audio forwarding.
.TP
.B \-\-no\-cleanup
By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.
@@ -210,8 +198,8 @@ This option disables this behavior.
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-mirror
Do not mirror device video or audio on the computer (only when recording or V4L2 sink is enabled).
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP
.B \-\-no\-key\-repeat
@@ -225,10 +213,6 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically
.B \-\-no\-power\-on
Do not power on the device on start.
.TP
.B \-\-no\-video
Disable video forwarding.
.TP
.B \-\-otg
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.
@@ -587,7 +571,7 @@ Copyright \(co 2018 Genymobile
Genymobile
.UE
Copyright \(co 2018\-2023
Copyright \(co 2018\-2022
.MT rom@rom1v.com
Romain Vimont
.ME

View File

@@ -204,7 +204,6 @@ sc_adb_parse_device_ip(char *str) {
while (str[idx_line] != '\0') {
char *line = &str[idx_line];
size_t len = strcspn(line, "\n");
bool is_last_line = line[len] == '\0';
// The same, but without any trailing '\r'
size_t line_len = sc_str_remove_trailing_cr(line, len);
@@ -216,12 +215,12 @@ sc_adb_parse_device_ip(char *str) {
return ip;
}
if (is_last_line) {
break;
}
idx_line += len;
// The next line starts after the '\n'
idx_line += len + 1;
if (str[idx_line] != '\0') {
// The next line starts after the '\n'
++idx_line;
}
}
return NULL;

View File

@@ -1,6 +1,5 @@
#include "audio_player.h"
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include "util/log.h"
@@ -19,38 +18,39 @@
* possible while avoiding buffer underrun (i.e. not being able to provide
* samples when requested).
*
* The player aims to feed the audio output with as little latency as possible
* while avoiding buffer underrun. To achieve this, it attempts to maintain the
* average buffering (the number of samples present in the buffer) around a
* target value. If this target buffering is too low, then buffer underrun will
* occur frequently. If it is too high, then latency will become unacceptable.
* This target value is configured using the scrcpy option --audio-buffer.
* For that purpose, it attempts to keep the average buffering (the number of
* samples present in the buffer) around a target value. If this target
* buffering is too low, then buffer underrun will occur often. If it is too
* high, then latency will be increased (this can be used on purpose to delay
* the audio playback). This value can be configured (in milliseconds) via the
* scrcpy option --audio-buffer.
*
* The player cannot adjust the sample input rate (it receives samples produced
* in real-time) or the sample output rate (it must provide samples as
* requested by the audio output callback). Therefore, it may only apply
* compensation by resampling (converting _m_ input samples to _n_ output
* samples).
* The player could not adjust the sample input rate (it receives samples
* produced in real-time) nor the sample output rate (it must provide samples
* as requested by the audio output callback). Therefore, it may only apply
* compensation by resampling (convert m input samples to n output samples).
*
* The compensation itself is applied by libswresample (FFmpeg). It is
* configured using swr_set_compensation(). An important work for the player
* is to estimate the compensation value regularly and apply it.
* The compensation itself is applied by swresample (FFmpeg). It is configured
* by swr_set_compensation(). An important work for the player is thus to
* estimate regularly the compensation value to configure.
*
* The estimated buffering level is the result of averaging the "natural"
* buffering (samples are produced and consumed by blocks, so it must be
* smoothed), and making instant adjustments resulting of its own actions
* (explicit compensation and silence insertion on underflow), which are not
* smoothed.
* Basically, the player wants to estimate the current buffering level: it is
* the result of an average of the "natural" buffering (samples are produced
* and consumed by blocks, so it must be smoothed), and by instant adjustments
* resulting of its own actions (explicit compensation and silence insertion on
* underflow), which are not smoothed.
*
* Buffer underflow events can occur when packets arrive too late. In that case,
* the player inserts silence. Once the packets finally arrive (late), one
* strategy could be to drop the samples that were replaced by silence, in
* order to keep a minimal latency. However, dropping samples in case of buffer
* underflow is inadvisable, as it would temporarily increase the underflow
* even more and cause very noticeable audio glitches.
* Buffer underflow events may occur when packets arrive too late. In that
* case, the player is inserting silence. Once the packets finally arrive
* (late), one strategy could be to drop the samples that were replaced by
* silence, in order to keep a minimal latency. However, dropping samples in
* case of buffer underflow is not a good strategy: it would temporarily
* increase the underflow even more, and cause very audible audio glitches.
*
* Therefore, the player doesn't drop any sample on underflow. The compensation
* mechanism will absorb the delay introduced by the inserted silence.
* Therefore, the player don't drop any sample on underflow, the stream is just
* delayed by the number of silent samples inserted. But in return, these
* additional samples will increase buffering (latency) when the late packets
* will arrive, so they will be taken into account by compensation.
*/
/** Downcast frame_sink to sc_audio_player */
@@ -59,32 +59,46 @@
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
#define SC_SDL_SAMPLE_FMT AUDIO_F32
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 240 // 5ms at 48000Hz
static inline uint32_t
bytes_to_samples(struct sc_audio_player *ap, size_t bytes) {
assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0);
return bytes / (ap->nb_channels * ap->out_bytes_per_sample);
}
static inline size_t
samples_to_bytes(struct sc_audio_player *ap, uint32_t samples) {
return samples * ap->nb_channels * ap->out_bytes_per_sample;
}
static void SDLCALL
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 audiobuf is protected
// the bytebuf is protected
assert(len_int > 0);
size_t len = len_int;
uint32_t count = TO_SAMPLES(len);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
LOGD("[Audio] SDL callback requests %" PRIu32 " samples",
bytes_to_samples(ap, len));
#endif
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
if (!ap->played) {
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
// Part of the buffering is handled by inserting initial silence. The
// remaining (margin) last samples will be handled by compensation.
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms
if (buffered_samples + margin < ap->target_buffering) {
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", count);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Inserting initial buffering silence: %" PRIu32
" samples", bytes_to_samples(ap, len));
#endif
// Delay playback starting to reach the target buffering. Fill the
// whole buffer with silence (len is small compared to the
// arbitrary margin value).
@@ -93,25 +107,27 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
}
}
uint32_t read = MIN(buffered_samples, count);
size_t read = MIN(read_avail, len);
if (read) {
sc_audiobuf_read(&ap->buf, stream, read);
sc_bytebuf_read(&ap->buf, stream, read);
}
if (read < count) {
uint32_t silence = count - read;
// Insert silence. In theory, the inserted silent samples replace the
// missing real samples, which will arrive later, so they should be
// dropped to keep the latency minimal. However, this would cause very
// audible glitches, so let the clock compensation restore the target
// latency.
if (read < len) {
size_t silence_bytes = len - read;
uint32_t silence_samples = bytes_to_samples(ap, silence_bytes);
// Insert silence. In theory, the inserted silent replaces the missing
// samples, which will arrive later, so they should be dropped to keep
// the latency minimal. However, this would cause very audible
// glitches, so let the clock compensation restore the target latency.
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(stream + read, 0, TO_BYTES(silence));
silence_samples);
#endif
memset(stream + read, 0, silence_bytes);
if (ap->received) {
// Inserting additional samples immediately increases buffering
ap->underflow += silence;
ap->avg_buffering.avg += silence_samples;
}
}
@@ -119,8 +135,8 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
}
static uint8_t *
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
size_t min_buf_size = TO_BYTES(min_samples);
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
size_t min_buf_size = samples_to_bytes(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);
@@ -144,8 +160,8 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
SwrContext *swr_ctx = ap->swr_ctx;
int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same.
// Add more space (256) for clock compensation.
// No need to av_rescale_rnd(), input and output sample rates are the same
// Add more space (256) for clock compensation
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples);
@@ -163,6 +179,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// swr_convert() returns the number of samples which would have been
// written if the buffer was big enough.
uint32_t samples_written = MIN(ret, dst_nb_samples);
size_t swr_buf_size = samples_to_bytes(ap, samples_written);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written);
#endif
@@ -170,105 +187,65 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// Since this function is the only writer, the current available space is
// at least the previous available space. In practice, it should almost
// always be possible to write without lock.
bool lockless_write = samples_written <= ap->previous_can_write;
bool lockless_write = swr_buf_size <= ap->previous_write_avail;
if (lockless_write) {
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written);
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
}
SDL_LockAudioDevice(ap->device);
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
if (lockless_write) {
sc_audiobuf_commit_write(&ap->buf, samples_written);
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
} else {
uint32_t can_write = sc_audiobuf_can_write(&ap->buf);
if (samples_written > can_write) {
// Entering this branch is very unlikely, the audio buffer is
// allocated with a size sufficient to store 1 second more than the
// target buffering. If this happens, though, we have to skip old
// samples.
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
if (samples_written > cap) {
// Very very unlikely: a single resampled frame should never
// exceed the audio buffer size (or something is very wrong).
// Take care to keep full samples
size_t align = ap->nb_channels * ap->out_bytes_per_sample;
size_t write_avail =
sc_bytebuf_write_available(&ap->buf) / align * align;
if (swr_buf_size > write_avail) {
// Skip old samples
size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align;
if (swr_buf_size > cap) {
// Ignore the first bytes in swr_buf
swr_buf += TO_BYTES(samples_written - cap);
// This change in samples_written will impact the
// instant_compensation below
samples_written = cap;
swr_buf += swr_buf_size - cap;
swr_buf_size = cap;
}
assert(samples_written >= can_write);
if (samples_written > can_write) {
uint32_t skip_samples = samples_written - can_write;
assert(buffered_samples >= skip_samples);
sc_audiobuf_skip(&ap->buf, skip_samples);
buffered_samples -= skip_samples;
if (ap->played) {
// Dropping input samples instantly decreases buffering
ap->avg_buffering.avg -= skip_samples;
}
assert(swr_buf_size > write_avail);
if (swr_buf_size - write_avail > 0) {
sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail);
}
// It should remain exactly the expected size to write the new
// samples.
assert(sc_audiobuf_can_write(&ap->buf) == samples_written);
}
sc_audiobuf_write(&ap->buf, swr_buf, samples_written);
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
}
buffered_samples += samples_written;
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf));
assert(samples_to_bytes(ap, buffered_samples)
== sc_bytebuf_read_available(&ap->buf));
// Read with lock held, to be used after unlocking
bool played = ap->played;
uint32_t underflow = ap->underflow;
if (played) {
uint32_t max_buffered_samples = ap->target_buffering
+ 12 * ap->output_buffer
+ 12 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES
+ ap->target_buffering / 10;
if (buffered_samples > max_buffered_samples) {
uint32_t skip_samples = buffered_samples - max_buffered_samples;
sc_audiobuf_skip(&ap->buf, skip_samples);
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
sc_bytebuf_skip(&ap->buf, skip_bytes);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
" samples", skip_samples);
}
// reset (the current value was copied to a local variable)
ap->underflow = 0;
} else {
// SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
uint32_t max_initial_buffering = ap->target_buffering
+ 2 * ap->output_buffer;
if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering;
sc_audiobuf_skip(&ap->buf, skip_samples);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
skip_samples);
#endif
}
}
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
ap->received = true;
SDL_UnlockAudioDevice(ap->device);
if (played) {
// Number of samples added (or removed, if negative) for compensation
int32_t instant_compensation =
(int32_t) samples_written - frame->nb_samples;
int32_t inserted_silence = (int32_t) underflow;
// The compensation must apply instantly, it must not be smoothed
ap->avg_buffering.avg += instant_compensation + inserted_silence;
ap->avg_buffering.avg += instant_compensation;
// However, the buffering level must be smoothed
sc_average_push(&ap->avg_buffering, buffered_samples);
@@ -277,7 +254,29 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f",
buffered_samples, sc_average_get(&ap->avg_buffering));
#endif
} else {
// SDL playback not started yet, do not accumulate more than
// max_initial_buffering samples, this would cause unnecessary delay
// (and glitches to compensate) on start.
uint32_t max_initial_buffering = ap->target_buffering
+ 2 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES;
if (buffered_samples > max_initial_buffering) {
uint32_t skip_samples = buffered_samples - max_initial_buffering;
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
sc_bytebuf_skip(&ap->buf, skip_bytes);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
skip_samples);
#endif
}
}
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
ap->received = true;
SDL_UnlockAudioDevice(ap->device);
if (played) {
ap->samples_since_resync += samples_written;
if (ap->samples_since_resync >= ap->sample_rate) {
// Recompute compensation every second
@@ -285,10 +284,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
float avg = sc_average_get(&ap->avg_buffering);
int diff = ap->target_buffering - avg;
if (abs(diff) < (int) ap->sample_rate / 1000) {
// Do not compensate for less than 1ms, the error is just noise
diff = 0;
} else if (diff < 0 && buffered_samples < ap->target_buffering) {
if (diff < 0 && buffered_samples < ap->target_buffering) {
// Do not accelerate if the instant buffering level is below
// the average, this would increase underflow
diff = 0;
@@ -299,18 +295,13 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
// Limit compensation rate to 2%
int abs_max_diff = distance / 50;
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
" compensation=%d", ap->target_buffering, avg,
buffered_samples, diff);
if (diff != ap->compensation) {
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
} else {
ap->compensation = diff;
}
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Average buffering=%f, compensation %d", avg, diff);
#endif
int ret = swr_set_compensation(swr_ctx, diff, distance);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
}
}
}
@@ -331,28 +322,11 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
unsigned nb_channels = tmp;
#endif
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);
ap->sample_rate = ctx->sample_rate;
ap->nb_channels = nb_channels;
ap->out_bytes_per_sample = out_bytes_per_sample;
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
/ SC_TICK_FREQ;
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
/ SC_TICK_FREQ;
assert(aout_samples <= 0xFFFF);
ap->output_buffer = (uint16_t) aout_samples;
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels,
.samples = aout_samples,
.samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
.callback = sc_audio_player_sdl_callback,
.userdata = ap,
};
@@ -371,6 +345,11 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
}
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);
@@ -393,27 +372,34 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
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;
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
/ SC_TICK_FREQ;
// Use a ring-buffer of the target buffering size plus 1 second between the
// producer and the consumer. It's too big on purpose, to guarantee that
// the producer and the consumer will be able to access it in parallel
// without locking.
size_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
size_t bytebuf_samples = ap->target_buffering + ap->sample_rate;
size_t bytebuf_size = samples_to_bytes(ap, bytebuf_samples);
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
if (!ok) {
goto error_free_swr_ctx;
}
size_t initial_swr_buf_size = TO_BYTES(4096);
size_t initial_swr_buf_size = samples_to_bytes(ap, 4096);
ap->swr_buf = malloc(initial_swr_buf_size);
if (!ap->swr_buf) {
LOG_OOM();
goto error_destroy_audiobuf;
goto error_destroy_bytebuf;
}
ap->swr_buf_alloc_size = initial_swr_buf_size;
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
// Samples are produced and consumed by blocks, so the buffering must be
// smoothed to get a relatively stable value.
@@ -422,8 +408,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
ap->received = false;
ap->played = false;
ap->underflow = 0;
ap->compensation = 0;
// The thread calling open() is the thread calling push(), which fills the
// audio buffer consumed by the SDL audio thread.
@@ -437,8 +421,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
return true;
error_destroy_audiobuf:
sc_audiobuf_destroy(&ap->buf);
error_destroy_bytebuf:
sc_bytebuf_destroy(&ap->buf);
error_free_swr_ctx:
swr_free(&ap->swr_ctx);
error_close_audio_device:
@@ -456,15 +440,13 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
SDL_CloseAudioDevice(ap->device);
free(ap->swr_buf);
sc_audiobuf_destroy(&ap->buf);
sc_bytebuf_destroy(&ap->buf);
swr_free(&ap->swr_ctx);
}
void
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
sc_tick output_buffer_duration) {
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) {
ap->target_buffering_delay = target_buffering;
ap->output_buffer_duration = output_buffer_duration;
static const struct sc_frame_sink_ops ops = {
.open = sc_audio_player_frame_sink_open,

View File

@@ -5,8 +5,8 @@
#include <stdbool.h>
#include "trait/frame_sink.h"
#include <util/audiobuf.h>
#include <util/average.h>
#include <util/bytebuf.h>
#include <util/thread.h>
#include <util/tick.h>
@@ -27,17 +27,13 @@ struct sc_audio_player {
sc_tick target_buffering_delay;
uint32_t target_buffering; // in samples
// SDL audio output buffer size.
sc_tick output_buffer_duration;
uint16_t output_buffer;
// Audio buffer to communicate between the receiver and the SDL audio
// callback (protected by SDL_AudioDeviceLock())
struct sc_audiobuf buf;
struct sc_bytebuf buf;
// The previous empty space in the buffer (only used by the receiver
// thread)
uint32_t previous_can_write;
// The previous number of bytes available in the buffer (only used by the
// receiver thread)
size_t previous_write_avail;
// Resampler (only used from the receiver thread)
struct SwrContext *swr_ctx;
@@ -60,13 +56,6 @@ struct sc_audio_player {
// (only used by the receiver thread)
uint32_t samples_since_resync;
// Number of silence samples inserted since the last received packet
// (protected by SDL_AudioDeviceLock())
uint32_t underflow;
// Current applied compensation value (only used by the receiver thread)
int compensation;
// Set to true the first time a sample is received (protected by
// SDL_AudioDeviceLock())
bool received;
@@ -84,7 +73,6 @@ struct sc_audio_player_callbacks {
};
void
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
sc_tick audio_output_buffer);
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering);
#endif

View File

@@ -18,7 +18,8 @@
#define STR(x) STR_IMPL_(x)
enum {
OPT_BIT_RATE = 1000,
OPT_RENDER_EXPIRED_FRAMES = 1000,
OPT_BIT_RATE,
OPT_WINDOW_TITLE,
OPT_PUSH_TARGET,
OPT_ALWAYS_ON_TOP,
@@ -71,9 +72,6 @@ enum {
OPT_LIST_DISPLAYS,
OPT_REQUIRE_AUDIO,
OPT_AUDIO_BUFFER,
OPT_AUDIO_OUTPUT_BUFFER,
OPT_NO_DISPLAY,
OPT_NO_VIDEO,
};
struct sc_option {
@@ -121,7 +119,7 @@ static const struct sc_option options[] = {
.argdesc = "value",
.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 128K (128000).",
"Default is 196K (196000).",
},
{
.longopt_id = OPT_AUDIO_BUFFER,
@@ -129,19 +127,9 @@ static const struct sc_option options[] = {
.argdesc = "ms",
.text = "Configure the audio buffering delay (in milliseconds).\n"
"Lower values decrease the latency, but increase the "
"likelyhood of buffer underrun (causing audio glitches).\n"
"likelyhood of buffer underruns (causing audio glitches).\n"
"Default is 50.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
@@ -382,14 +370,9 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'N',
.longopt = "no-mirror",
.text = "Do not mirror device video or audio on the computer (only "
"when recording or V4L2 sink is enabled).",
},
{
// deprecated
.longopt_id = OPT_NO_DISPLAY,
.longopt = "no-display",
.text = "Do not display device (only when screen recording or V4L2 "
"sink is enabled).",
},
{
.longopt_id = OPT_NO_KEY_REPEAT,
@@ -408,11 +391,6 @@ static const struct sc_option options[] = {
.longopt = "no-power-on",
.text = "Do not power on the device on start.",
},
{
.longopt_id = OPT_NO_VIDEO,
.longopt = "no-video",
.text = "Disable video forwarding.",
},
{
.longopt_id = OPT_OTG,
.longopt = "otg",
@@ -493,6 +471,11 @@ static const struct sc_option options[] = {
"\"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
},
{
// deprecated
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
.longopt = "render-expired-frames",
},
{
.longopt_id = OPT_REQUIRE_AUDIO,
.longopt = "require-audio",
@@ -1227,19 +1210,6 @@ parse_buffering_time(const char *s, sc_tick *tick) {
return true;
}
static bool
parse_audio_output_buffer(const char *s, sc_tick *tick) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 1000,
"audio output buffer");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_MS(value);
return true;
}
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
@@ -1479,39 +1449,18 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
}
#endif
static enum sc_record_format
get_record_format(const char *name) {
if (!strcmp(name, "mp4")) {
return SC_RECORD_FORMAT_MP4;
}
if (!strcmp(name, "mkv")) {
return SC_RECORD_FORMAT_MKV;
}
if (!strcmp(name, "m4a")) {
return SC_RECORD_FORMAT_M4A;
}
if (!strcmp(name, "mka")) {
return SC_RECORD_FORMAT_MKA;
}
if (!strcmp(name, "opus")) {
return SC_RECORD_FORMAT_OPUS;
}
if (!strcmp(name, "aac")) {
return SC_RECORD_FORMAT_AAC;
}
return 0;
}
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
enum sc_record_format fmt = get_record_format(optarg);
if (!fmt) {
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
if (!strcmp(optarg, "mp4")) {
*format = SC_RECORD_FORMAT_MP4;
return true;
}
*format = fmt;
return true;
if (!strcmp(optarg, "mkv")) {
*format = SC_RECORD_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static bool
@@ -1531,13 +1480,18 @@ parse_port(const char *optarg, uint16_t *port) {
static enum sc_record_format
guess_record_format(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = dot + 1;
return get_record_format(ext);
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return SC_RECORD_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return SC_RECORD_FORMAT_MKV;
}
return 0;
}
static bool
@@ -1587,9 +1541,8 @@ 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 OPT_BIT_RATE:
LOGE("--bit-rate has been removed, "
"use --video-bit-rate or --audio-bit-rate.");
return false;
LOGW("--bit-rate is deprecated, use --video-bit-rate instead.");
// fall through
case 'b':
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
return false;
@@ -1617,6 +1570,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case 'f':
opts->fullscreen = true;
break;
case 'F':
LOGW("Deprecated option -F. Use --record-format instead.");
// fall through
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false;
@@ -1670,11 +1626,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case 'n':
opts->control = false;
break;
case OPT_NO_DISPLAY:
LOGW("--no-display is deprecated, use --no-mirror instead.");
// fall through
case 'N':
opts->mirror = false;
opts->display = false;
break;
case 'p':
if (!parse_port_range(optarg, &opts->port_range)) {
@@ -1707,6 +1660,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case 'w':
opts->stay_awake = true;
break;
case OPT_RENDER_EXPIRED_FRAMES:
LOGW("Option --render-expired-frames has been removed. This "
"flag has been ignored.");
break;
case OPT_WINDOW_TITLE:
opts->window_title = optarg;
break;
@@ -1765,9 +1722,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
LOGE("--codec-options has been removed, "
"use --video-codec-options or --audio-codec-options.");
return false;
LOGW("--codec-options is deprecated, use --video-codec-options "
"instead.");
// fall through
case OPT_VIDEO_CODEC_OPTIONS:
opts->video_codec_options = optarg;
break;
@@ -1775,9 +1732,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->audio_codec_options = optarg;
break;
case OPT_ENCODER:
LOGE("--encoder has been removed, "
"use --video-encoder or --audio-encoder.");
return false;
LOGW("--encoder is deprecated, use --video-encoder instead.");
// fall through
case OPT_VIDEO_ENCODER:
opts->video_encoder = optarg;
break;
@@ -1819,9 +1775,6 @@ 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_VIDEO:
opts->video = false;
break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
@@ -1835,9 +1788,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->start_fps_counter = true;
break;
case OPT_CODEC:
LOGE("--codec has been removed, "
"use --video-codec or --audio-codec.");
return false;
LOGW("--codec is deprecated, use --video-codec instead.");
// fall through
case OPT_VIDEO_CODEC:
if (!parse_video_codec(optarg, &opts->video_codec)) {
return false;
@@ -1889,12 +1841,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_AUDIO_OUTPUT_BUFFER:
if (!parse_audio_output_buffer(optarg,
&opts->audio_output_buffer)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;
@@ -1924,8 +1870,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#ifdef HAVE_V4L2
if (!opts->mirror && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-mirror requires either screen recording (-r/--record)"
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-display requires either screen recording (-r/--record)"
" or sink to v4l2loopback device (--v4l2-sink)");
return false;
}
@@ -1949,14 +1895,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
#else
if (!opts->mirror && !opts->record_filename) {
LOGE("-N/--no-mirror requires screen recording (-r/--record)");
if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
#endif
if (opts->audio && !opts->mirror && !opts->record_filename) {
LOGI("No mirror and no recording: audio disabled");
if (opts->audio && !opts->display && !opts->record_filename) {
LOGI("No display and no recording: audio disabled");
opts->audio = false;
}
@@ -1971,41 +1917,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->record_filename) {
if (opts->record_filename && !opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
LOGE("No format specified for \"%s\" "
"(try with --record-format=mkv)",
opts->record_filename);
return false;
}
}
if (opts->audio_codec == SC_CODEC_RAW) {
LOGW("Recording does not support RAW audio codec");
LOGE("No format specified for \"%s\" "
"(try with --record-format=mkv)",
opts->record_filename);
return false;
}
}
if (opts->video
&& sc_record_format_is_audio_only(opts->record_format)) {
LOGE("Audio container does not support video stream");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_OPUS
&& opts->audio_codec != SC_CODEC_OPUS) {
LOGE("Recording to OPUS file requires an OPUS audio stream "
"(try with --audio-codec=opus)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_AAC
&& opts->audio_codec != SC_CODEC_AAC) {
LOGE("Recording to AAC file requires an AAC audio stream "
"(try with --audio-codec=aac)");
return false;
}
if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) {
LOGW("Recording does not support RAW audio codec");
return false;
}
if (opts->audio_codec == SC_CODEC_RAW) {
@@ -2088,21 +2012,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
#ifdef HAVE_USB
if (!(opts->mirror && opts->video) && !opts->otg) {
#else
if (!(opts->mirror && opts->video)) {
#endif
// If video mirroring is disabled and OTG are disabled, then there is
// no way to control the device.
opts->control = false;
}
if (!opts->video) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
return true;
}

View File

@@ -1,36 +1,116 @@
#include "clock.h"
#include <assert.h>
#include "util/log.h"
#define SC_CLOCK_NDEBUG // comment to debug
#define SC_CLOCK_RANGE 32
void
sc_clock_init(struct sc_clock *clock) {
clock->range = 0;
clock->offset = 0;
clock->count = 0;
clock->head = 0;
clock->left_sum.system = 0;
clock->left_sum.stream = 0;
clock->right_sum.system = 0;
clock->right_sum.stream = 0;
}
// Estimate the affine function f(stream) = slope * stream + offset
static void
sc_clock_estimate(struct sc_clock *clock,
double *out_slope, sc_tick *out_offset) {
assert(clock->count);
if (clock->count == 1) {
// If there is only 1 point, we can't compute a slope. Assume it is 1.
struct sc_clock_point *single_point = &clock->right_sum;
*out_slope = 1;
*out_offset = single_point->system - single_point->stream;
return;
}
struct sc_clock_point left_avg = {
.system = clock->left_sum.system / (clock->count / 2),
.stream = clock->left_sum.stream / (clock->count / 2),
};
struct sc_clock_point right_avg = {
.system = clock->right_sum.system / ((clock->count + 1) / 2),
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
};
double slope = (double) (right_avg.system - left_avg.system)
/ (right_avg.stream - left_avg.stream);
if (clock->count < SC_CLOCK_RANGE) {
/* The first frames are typically received and decoded with more delay
* than the others, causing a wrong slope estimation on start. To
* compensate, assume an initial slope of 1, then progressively use the
* estimated slope. */
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
/ SC_CLOCK_RANGE;
}
struct sc_clock_point global_avg = {
.system = (clock->left_sum.system + clock->right_sum.system)
/ clock->count,
.stream = (clock->left_sum.stream + clock->right_sum.stream)
/ clock->count,
};
sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
*out_slope = slope;
*out_offset = offset;
}
void
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
if (clock->range < SC_CLOCK_RANGE) {
++clock->range;
struct sc_clock_point *point = &clock->points[clock->head];
if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
// One point passes from the right sum to the left sum
unsigned mid;
if (clock->count == SC_CLOCK_RANGE) {
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
} else {
// Only for the first frames
mid = clock->count / 2;
}
struct sc_clock_point *mid_point = &clock->points[mid];
clock->left_sum.system += mid_point->system;
clock->left_sum.stream += mid_point->stream;
clock->right_sum.system -= mid_point->system;
clock->right_sum.stream -= mid_point->stream;
}
sc_tick offset = system - stream;
clock->offset = ((clock->range - 1) * clock->offset + offset)
/ clock->range;
if (clock->count == SC_CLOCK_RANGE) {
// The current point overwrites the previous value in the circular
// array, update the left sum accordingly
clock->left_sum.system -= point->system;
clock->left_sum.stream -= point->stream;
} else {
++clock->count;
}
point->system = system;
point->stream = stream;
clock->right_sum.system += system;
clock->right_sum.stream += stream;
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
// Update estimation
sc_clock_estimate(clock, &clock->slope, &clock->offset);
#ifndef SC_CLOCK_NDEBUG
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
#endif
}
sc_tick
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
assert(clock->range); // sc_clock_update() must have been called
return stream + clock->offset;
assert(clock->count); // sc_clock_update() must have been called
return (sc_tick) (stream * clock->slope) + clock->offset;
}

View File

@@ -3,8 +3,13 @@
#include "common.h"
#include <assert.h>
#include "util/tick.h"
#define SC_CLOCK_RANGE 32
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
struct sc_clock_point {
sc_tick system;
sc_tick stream;
@@ -16,18 +21,40 @@ struct sc_clock_point {
*
* f(stream) = slope * stream + offset
*
* Theoretically, the slope encodes the drift between the device clock and the
* computer clock. It is expected to be very close to 1.
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
* of a frame expressed both in stream time and system time) in a circular
* array.
*
* Since the clock is used to estimate very close points in the future (which
* are reestimated on every clock update, see delay_buffer), the error caused
* by clock drift is totally negligible, so it is better to assume that the
* slope is 1 than to estimate it (the estimation error would be larger).
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
* point"). The slope of the estimated affine function is that of the line
* passing through these two points.
*
* Therefore, only the offset is estimated.
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
* points. The resulting affine function passes by this centroid.
*
* With a circular array, the rolling sums (and average) are quick to compute.
* In practice, the estimation is stable and the evolution is smooth.
*/
struct sc_clock {
unsigned range;
// Circular array
struct sc_clock_point points[SC_CLOCK_RANGE];
// Number of points in the array (count <= SC_CLOCK_RANGE)
unsigned count;
// Index of the next point to write
unsigned head;
// Sum of the first count/2 points
struct sc_clock_point left_sum;
// Sum of the last (count+1)/2 points
struct sc_clock_point right_sum;
// Estimated slope and offset
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
double slope;
sc_tick offset;
};

View File

@@ -25,12 +25,6 @@
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif
// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added
// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag
// n3.3).
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100)
# define SCRCPY_LAVC_HAS_AV1
#endif
// In ffmpeg/doc/APIchanges:
// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h

View File

@@ -12,19 +12,51 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
static bool
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) {
LOG_OOM();
return false;
}
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
av_frame_free(&decoder->frame);
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("Decoder '%s': could not open codec", decoder->name);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
decoder->ctx = ctx;
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
LOG_OOM();
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
if (!sc_frame_source_sinks_open(&decoder->frame_source,
decoder->codec_ctx)) {
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
return true;
}
@@ -33,6 +65,8 @@ static void
sc_decoder_close(struct sc_decoder *decoder) {
sc_frame_source_sinks_close(&decoder->frame_source);
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
}
static bool
@@ -43,7 +77,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
return true;
}
int ret = avcodec_send_packet(decoder->ctx, packet);
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Decoder '%s': could not send video packet: %d",
decoder->name, ret);
@@ -51,7 +85,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
}
for (;;) {
ret = avcodec_receive_frame(decoder->ctx, decoder->frame);
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
}
@@ -76,9 +110,9 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
}
static bool
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
struct sc_decoder *decoder = DOWNCAST(sink);
return sc_decoder_open(decoder, ctx);
return sc_decoder_open(decoder, codec);
}
static void

View File

@@ -16,7 +16,7 @@ struct sc_decoder {
const char *name; // must be statically allocated (e.g. a string literal)
AVCodecContext *ctx;
AVCodecContext *codec_ctx;
AVFrame *frame;
};

View File

@@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
sc_clock_update(&db->clock, sc_tick_now(), pts);
sc_cond_signal(&db->wait_cond);
if (db->first_frame_asap && db->clock.range == 1) {
if (db->first_frame_asap && db->clock.count == 1) {
sc_mutex_unlock(&db->mutex);
return sc_frame_source_sinks_push(&db->frame_source, frame);
}

View File

@@ -1,7 +1,6 @@
#include "demuxer.h"
#include <assert.h>
#include <libavutil/channel_layout.h>
#include <libavutil/time.h>
#include <unistd.h>
@@ -33,12 +32,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1:
#ifdef SCRCPY_LAVC_HAS_AV1
return AV_CODEC_ID_AV1;
#else
LOGE("AV1 not supported by this FFmpeg version");
return AV_CODEC_ID_NONE;
#endif
case SC_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC:
@@ -63,20 +57,6 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
return true;
}
static bool
sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
uint32_t *height) {
uint8_t data[8];
ssize_t r = net_recv_all(demuxer->socket, data, 8);
if (r < 8) {
return false;
}
*width = sc_read32be(data);
*height = sc_read32be(data + 4);
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
@@ -180,45 +160,10 @@ run_demuxer(void *data) {
goto end;
}
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
LOG_OOM();
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec)) {
goto end;
}
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
if (codec->type == AVMEDIA_TYPE_VIDEO) {
uint32_t width;
uint32_t height;
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
if (!ok) {
goto finally_free_context;
}
codec_ctx->width = width;
codec_ctx->height = height;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
// Hardcoded audio properties
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
#else
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
codec_ctx->channels = 2;
#endif
codec_ctx->sample_rate = 48000;
}
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
LOGE("Demuxer '%s': could not open codec", demuxer->name);
goto finally_free_context;
}
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
goto finally_free_context;
}
// 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;
@@ -255,7 +200,7 @@ run_demuxer(void *data) {
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
av_packet_unref(packet);
if (!ok) {
// The sink already logged its concrete error
LOGE("Demuxer '%s': could not process packet", demuxer->name);
break;
}
}
@@ -269,9 +214,6 @@ run_demuxer(void *data) {
av_packet_free(&packet);
finally_close_sinks:
sc_packet_source_sinks_close(&demuxer->packet_source);
finally_free_context:
// This also calls avcodec_close() internally
avcodec_free_context(&codec_ctx);
end:
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);

View File

@@ -1,185 +0,0 @@
#include "display.h"
#include <assert.h>
#include "util/log.h"
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
display->renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!display->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
return false;
}
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(display->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
display->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
LOGD("Creating OpenGL Core Profile context");
display->gl_context = SDL_GL_CreateContext(window);
if (!display->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
SDL_DestroyRenderer(display->renderer);
return false;
}
#endif
struct sc_opengl *gl = &display->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
display->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer");
}
return true;
}
void
sc_display_destroy(struct sc_display *display) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
#endif
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
SDL_DestroyRenderer(display->renderer);
}
static SDL_Texture *
sc_display_create_texture(struct sc_display *display,
struct sc_size size) {
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (display->mipmaps) {
struct sc_opengl *gl = &display->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
SDL_GL_UnbindTexture(texture);
}
return texture;
}
bool
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
display->texture = sc_display_create_texture(display, size);
if (!display->texture) {
return false;
}
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
return true;
}
bool
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (ret) {
LOGE("Could not update texture: %s", SDL_GetError());
return false;
}
if (display->mipmaps) {
SDL_GL_BindTexture(display->texture, NULL, NULL);
display->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(display->texture);
}
return true;
}
bool
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation) {
SDL_RenderClear(display->renderer);
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture;
if (rotation == 0) {
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return false;
}
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - rotation) % 4;
double angle = 90 * cw_rotation;
const SDL_Rect *dstrect = NULL;
SDL_Rect rect;
if (rotation & 1) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
assert(rotation == 2);
dstrect = geometry;
}
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
NULL, 0);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return false;
}
}
SDL_RenderPresent(display->renderer);
return true;
}

View File

@@ -1,45 +0,0 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL.h>
#include "coords.h"
#include "opengl.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_display {
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext *gl_context;
#endif
bool mipmaps;
};
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
bool
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
bool
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
bool
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation);
#endif

View File

@@ -5,4 +5,3 @@
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)

View File

@@ -172,18 +172,14 @@ sc_file_pusher_start(struct sc_file_pusher *fp) {
void
sc_file_pusher_stop(struct sc_file_pusher *fp) {
if (fp->initialized) {
sc_mutex_lock(&fp->mutex);
fp->stopped = true;
sc_cond_signal(&fp->event_cond);
sc_intr_interrupt(&fp->intr);
sc_mutex_unlock(&fp->mutex);
}
sc_mutex_lock(&fp->mutex);
fp->stopped = true;
sc_cond_signal(&fp->event_cond);
sc_intr_interrupt(&fp->intr);
sc_mutex_unlock(&fp->mutex);
}
void
sc_file_pusher_join(struct sc_file_pusher *fp) {
if (fp->initialized) {
sc_thread_join(&fp->thread, NULL);
}
sc_thread_join(&fp->thread, NULL);
}

View File

@@ -96,7 +96,6 @@ run_fps_counter(void *data) {
bool
sc_fps_counter_start(struct sc_fps_counter *counter) {
sc_mutex_lock(&counter->mutex);
counter->interrupted = false;
counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL;
counter->nr_rendered = 0;
counter->nr_skipped = 0;

View File

@@ -797,8 +797,7 @@ sc_input_manager_process_file(struct sc_input_manager *im,
}
void
sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event) {
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
bool control = im->controller;
switch (event->type) {
case SDL_TEXTINPUT:

View File

@@ -61,7 +61,6 @@ sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params);
void
sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event);
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event);
#endif

View File

@@ -44,7 +44,6 @@ const struct scrcpy_options scrcpy_options_default = {
.display_buffer = 0,
.v4l2_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
#ifdef HAVE_USB
.otg = false,
#endif
@@ -52,7 +51,7 @@ const struct scrcpy_options scrcpy_options_default = {
.fullscreen = false,
.always_on_top = false,
.control = true,
.mirror = true,
.display = true,
.turn_screen_off = false,
.key_inject_mode = SC_KEY_INJECT_MODE_MIXED,
.window_borderless = false,
@@ -73,7 +72,6 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.video = true,
.audio = true,
.require_audio = false,
.list_encoders = false,

View File

@@ -21,20 +21,8 @@ enum sc_record_format {
SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV,
SC_RECORD_FORMAT_M4A,
SC_RECORD_FORMAT_MKA,
SC_RECORD_FORMAT_OPUS,
SC_RECORD_FORMAT_AAC,
};
static inline bool
sc_record_format_is_audio_only(enum sc_record_format fmt) {
return fmt == SC_RECORD_FORMAT_M4A
|| fmt == SC_RECORD_FORMAT_MKA
|| fmt == SC_RECORD_FORMAT_OPUS
|| fmt == SC_RECORD_FORMAT_AAC;
}
enum sc_codec {
SC_CODEC_H264,
SC_CODEC_H265,
@@ -139,7 +127,6 @@ struct scrcpy_options {
sc_tick display_buffer;
sc_tick v4l2_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
#ifdef HAVE_USB
bool otg;
#endif
@@ -147,7 +134,7 @@ struct scrcpy_options {
bool fullscreen;
bool always_on_top;
bool control;
bool mirror;
bool display;
bool turn_screen_off;
enum sc_key_inject_mode key_inject_mode;
bool window_borderless;
@@ -168,7 +155,6 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool video;
bool audio;
bool require_audio;
bool list_encoders;

View File

@@ -60,17 +60,9 @@ sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
static const char *
sc_recorder_get_format_name(enum sc_record_format format) {
switch (format) {
case SC_RECORD_FORMAT_MP4:
case SC_RECORD_FORMAT_M4A:
case SC_RECORD_FORMAT_AAC:
return "mp4";
case SC_RECORD_FORMAT_MKV:
case SC_RECORD_FORMAT_MKA:
return "matroska";
case SC_RECORD_FORMAT_OPUS:
return "opus";
default:
return NULL;
case SC_RECORD_FORMAT_MP4: return "mp4";
case SC_RECORD_FORMAT_MKV: return "matroska";
default: return NULL;
}
}
@@ -158,9 +150,75 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
avformat_free_context(recorder->ctx);
}
static bool
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
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 (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 (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
@@ -178,13 +236,11 @@ static bool
sc_recorder_process_header(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && (!recorder->video_init
|| !recorder->audio_init
|| sc_recorder_has_empty_queues(recorder))) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->stopped);
// If the recorder is stopped, don't process anything if there are not
// at least video packets
@@ -192,11 +248,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
return false;
}
AVPacket *video_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->video);
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
}
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue);
AVPacket *audio_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
@@ -208,19 +260,17 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
int ret = false;
if (video_pkt) {
if (video_pkt->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
}
if (video_pkt->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);
if (!ok) {
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);
if (!ok) {
goto end;
}
if (audio_pkt) {
@@ -232,13 +282,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
assert(recorder->audio_stream_index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
if (!ok) {
goto end;
}
}
bool ok = avformat_write_header(recorder->ctx, NULL) >= 0;
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) {
LOGE("Failed to write header to %s", recorder->filename);
goto end;
@@ -247,9 +297,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
ret = true;
end:
if (video_pkt) {
av_packet_free(&video_pkt);
}
av_packet_free(&video_pkt);
if (audio_pkt) {
av_packet_free(&audio_pkt);
}
@@ -279,8 +327,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) {
if (recorder->video && !video_pkt &&
!sc_vecdeque_is_empty(&recorder->video_queue)) {
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed
break;
}
@@ -295,11 +342,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
// If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping.
// If there is no video, then the video_queue will remain empty forever
// and video_pkt will always be NULL.
assert(recorder->video || (!video_pkt
&& sc_vecdeque_is_empty(&recorder->video_queue)));
// 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
@@ -341,9 +383,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
if (!recorder->audio) {
assert(video_pkt);
pts_origin = video_pkt->pts;
} else if (!recorder->video) {
assert(audio_pkt);
pts_origin = audio_pkt->pts;
} else if (video_pkt && audio_pkt) {
pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
} else if (recorder->stopped) {
@@ -441,6 +480,22 @@ sc_recorder_record(struct sc_recorder *recorder) {
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;
@@ -481,10 +536,9 @@ run_recorder(void *data) {
static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) {
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
// only written from this thread, no need to lock
assert(!recorder->video_init);
assert(codec);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
@@ -492,21 +546,7 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
return false;
}
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
if (!stream) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
if (r < 0) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
recorder->video_stream_index = stream->index;
recorder->video_init = true;
recorder->video_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
@@ -516,8 +556,6 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
static void
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
// only written from this thread, no need to lock
assert(recorder->video_init);
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
@@ -530,8 +568,6 @@ static bool
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
// only written from this thread, no need to lock
assert(recorder->video_init);
sc_mutex_lock(&recorder->mutex);
@@ -565,29 +601,15 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
static bool
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) {
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_init);
assert(!recorder->audio_disabled);
assert(codec);
sc_mutex_lock(&recorder->mutex);
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
if (!stream) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
if (r < 0) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
recorder->audio_stream_index = stream->index;
recorder->audio_init = true;
recorder->audio_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
@@ -599,7 +621,7 @@ 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_init);
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
@@ -614,7 +636,7 @@ sc_recorder_audio_packet_sink_push(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_init);
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
@@ -651,20 +673,21 @@ 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_init);
assert(!recorder->audio_disabled);
assert(!recorder->audio_codec);
LOGW("Audio stream recording disabled");
sc_mutex_lock(&recorder->mutex);
recorder->audio = false;
recorder->audio_init = true;
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, bool video, bool audio,
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) {
@@ -687,35 +710,33 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
goto error_queue_cond_destroy;
}
assert(video || audio);
recorder->video = video;
recorder->audio = audio;
sc_vecdeque_init(&recorder->video_queue);
sc_vecdeque_init(&recorder->audio_queue);
recorder->stopped = false;
recorder->video_init = false;
recorder->audio_init = 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;
assert(cbs && cbs->on_ended);
recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata;
if (video) {
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,
};
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->video_packet_sink.ops = &video_ops;
}
recorder->video_packet_sink.ops = &video_ops;
if (audio) {
static const struct sc_packet_sink_ops audio_ops = {

View File

@@ -27,11 +27,11 @@ struct sc_recorder {
* may access it without data races.
*/
bool audio;
bool video;
char *filename;
enum sc_record_format format;
AVFormatContext *ctx;
struct sc_size declared_frame_size;
sc_thread thread;
sc_mutex mutex;
@@ -43,8 +43,11 @@ struct sc_recorder {
// wake up the recorder thread once the video or audio codec is known
sc_cond stream_cond;
bool video_init;
bool audio_init;
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;
@@ -60,7 +63,8 @@ struct sc_recorder_callbacks {
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio,
enum sc_record_format format, bool audio,
struct sc_size declared_frame_size,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool

View File

@@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) {
}
static void
sdl_configure(bool mirror, bool disable_screensaver) {
sdl_configure(bool display, bool disable_screensaver) {
#ifdef _WIN32
// Clean up properly on Ctrl+C on Windows
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
@@ -146,7 +146,7 @@ sdl_configure(bool mirror, bool disable_screensaver) {
}
#endif // _WIN32
if (!mirror) {
if (!display) {
return;
}
@@ -175,9 +175,7 @@ event_loop(struct scrcpy *s) {
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
default:
if (!sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
}
sc_screen_handle_event(&s->screen, &event);
break;
}
}
@@ -345,7 +343,6 @@ scrcpy(struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
@@ -386,11 +383,13 @@ scrcpy(struct scrcpy_options *options) {
goto end;
}
if (options->mirror) {
if (options->display) {
sdl_set_hints(options->render_driver);
}
// Initialize SDL video and audio in addition if mirroring is enabled
if (options->video && SDL_Init(SDL_INIT_VIDEO)) {
// Initialize SDL video in addition if display is enabled
if (options->display) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
@@ -401,7 +400,7 @@ scrcpy(struct scrcpy_options *options) {
}
}
sdl_configure(options->mirror, options->disable_screensaver);
sdl_configure(options->display, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling
bool connected;
@@ -427,8 +426,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
assert(!options->control || options->mirror); // control implies mirror
if (options->control) {
if (options->display && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
@@ -437,13 +435,11 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
if (options->video) {
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);
}
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 = {
@@ -453,8 +449,8 @@ scrcpy(struct scrcpy_options *options) {
&audio_demuxer_cbs, options);
}
bool needs_video_decoder = options->mirror && options->video;
bool needs_audio_decoder = options->mirror && options->audio;
bool needs_video_decoder = options->display;
bool needs_audio_decoder = options->audio && options->display;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
#endif
@@ -474,8 +470,8 @@ scrcpy(struct scrcpy_options *options) {
.on_ended = sc_recorder_on_ended,
};
if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->video,
options->audio, &recorder_cbs, NULL)) {
options->record_format, options->audio,
info->frame_size, &recorder_cbs, NULL)) {
goto end;
}
recorder_initialized = true;
@@ -485,10 +481,8 @@ scrcpy(struct scrcpy_options *options) {
}
recorder_started = true;
if (options->video) {
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
}
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
if (options->audio) {
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
&s->recorder.audio_packet_sink);
@@ -650,7 +644,7 @@ aoa_hid_end:
// There is a controller if and only if control is enabled
assert(options->control == !!controller);
if (options->mirror) {
if (options->display) {
const char *window_title =
options->window_title ? options->window_title : info->device_name;
@@ -664,6 +658,7 @@ aoa_hid_end:
.clipboard_autosync = options->clipboard_autosync,
.shortcut_mods = &options->shortcut_mods,
.window_title = window_title,
.frame_size = info->frame_size,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
.window_y = options->window_y,
@@ -676,6 +671,11 @@ aoa_hid_end:
.start_fps_counter = options->start_fps_counter,
};
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
@@ -684,18 +684,10 @@ aoa_hid_end:
src = &s->display_buffer.frame_source;
}
if (options->video) {
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
sc_frame_source_add_sink(src, &s->screen.frame_sink);
if (options->audio) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_audio_player_init(&s->audio_player, options->audio_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
}
@@ -703,7 +695,8 @@ aoa_hid_end:
#ifdef HAVE_V4L2
if (options->v4l2_device) {
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) {
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
info->frame_size)) {
goto end;
}
@@ -720,15 +713,12 @@ aoa_hid_end:
}
#endif
// Now that the header values have been consumed, the socket(s) will
// receive the stream(s). Start the demuxer(s).
if (options->video) {
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
// now we consumed the header values, the socket receives the video stream
// start the video demuxer
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) {

View File

@@ -239,6 +239,33 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
}
}
static inline SDL_Texture *
create_texture(struct sc_screen *screen) {
SDL_Renderer *renderer = screen->renderer;
struct sc_size size = screen->frame_size;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
return NULL;
}
if (screen->mipmaps) {
struct sc_opengl *gl = &screen->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
SDL_GL_UnbindTexture(texture);
}
return texture;
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
@@ -249,11 +276,35 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
sc_screen_update_content_rect(screen);
}
bool ok = sc_display_render(&screen->display, &screen->rect,
screen->rotation);
(void) ok; // error already logged
SDL_RenderClear(screen->renderer);
if (screen->rotation == 0) {
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - screen->rotation) % 4;
double angle = 90 * cw_rotation;
SDL_Rect *dstrect = NULL;
SDL_Rect rect;
if (screen->rotation & 1) {
rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
rect.w = screen->rect.h;
rect.h = screen->rect.w;
dstrect = &rect;
} else {
assert(screen->rotation == 2);
dstrect = &screen->rect;
}
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
angle, NULL, 0);
}
SDL_RenderPresent(screen->renderer);
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@@ -284,25 +335,7 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
(void) ctx;
struct sc_screen *screen = DOWNCAST(sink);
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
// screen->frame_size is never used before the event is pushed, and the
// event acts as a memory barrier so it is safe without mutex
screen->frame_size.width = ctx->width;
screen->frame_size.height = ctx->height;
static SDL_Event event = {
.type = SC_EVENT_SCREEN_INIT_SIZE,
};
// Post the event on the UI thread (the texture must be created from there)
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGW("Could not post init size event: %s", SDL_GetError());
return false;
}
(void) screen;
#ifndef NDEBUG
screen->open = true;
#endif
@@ -377,10 +410,14 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_frame_buffer;
}
screen->frame_size = params->frame_size;
screen->rotation = params->rotation;
if (screen->rotation) {
LOGI("Initial display rotation set to %u", screen->rotation);
}
struct sc_size content_size =
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size;
uint32_t window_flags = SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE
@@ -400,11 +437,46 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
if (!ok) {
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
screen->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
struct sc_opengl *gl = &screen->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (params->mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
screen->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (params->mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
@@ -413,10 +485,18 @@ sc_screen_init(struct sc_screen *screen,
LOGW("Could not load icon");
}
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width,
params->frame_size.height);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
goto error_destroy_renderer;
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_display;
goto error_destroy_texture;
}
struct sc_input_manager_params im_params = {
@@ -451,8 +531,10 @@ sc_screen_init(struct sc_screen *screen,
return true;
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_texture:
SDL_DestroyTexture(screen->texture);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -508,8 +590,9 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_display_destroy(&screen->display);
av_frame_free(&screen->frame);
SDL_DestroyTexture(screen->texture);
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
@@ -572,38 +655,47 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) {
sc_screen_render(screen, true);
}
static bool
sc_screen_init_size(struct sc_screen *screen) {
// Before first frame
assert(!screen->has_frame);
// The requested size is passed via screen->frame_size
struct sc_size content_size =
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size;
return sc_display_set_texture_size(&screen->display, screen->frame_size);
}
// recreate the texture and resize the window if the frame size has changed
static bool
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) {
return true;
if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture);
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
LOGI("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
}
// frame dimension changed
screen->frame_size = new_frame_size;
return true;
}
struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
// write the frame into the texture
static void
update_texture(struct sc_screen *screen, const AVFrame *frame) {
SDL_UpdateYUVTexture(screen->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
sc_screen_update_content_rect(screen);
return sc_display_set_texture_size(&screen->display, screen->frame_size);
if (screen->mipmaps) {
SDL_GL_BindTexture(screen->texture, NULL, NULL);
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(screen->texture);
}
}
static bool
@@ -618,10 +710,7 @@ sc_screen_update_frame(struct sc_screen *screen) {
if (!prepare_for_frame(screen, new_frame_size)) {
return false;
}
if (!sc_display_update_texture(&screen->display, frame)) {
return false;
}
update_texture(screen, frame);
if (!screen->has_frame) {
screen->has_frame = true;
@@ -701,32 +790,22 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
}
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
void
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
case SC_EVENT_SCREEN_INIT_SIZE: {
// The initial size is passed via screen->frame_size
bool ok = sc_screen_init_size(screen);
if (!ok) {
LOGE("Could not initialize screen size");
return false;
}
return true;
}
case SC_EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
return false;
LOGW("Frame update failed\n");
}
return true;
return;
}
case SDL_WINDOWEVENT:
if (!screen->has_frame) {
// Do nothing
return true;
return;
}
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED:
@@ -757,7 +836,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
}
break;
}
return true;
return;
case SDL_KEYDOWN:
if (relative_mode) {
SDL_Keycode key = event->key.keysym.sym;
@@ -770,7 +849,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
screen->mouse_capture_key_pressed = 0;
}
// Mouse capture keys are never forwarded to the device
return true;
return;
}
}
break;
@@ -786,7 +865,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
sc_screen_toggle_mouse_capture(screen);
}
// Mouse capture keys are never forwarded to the device
return true;
return;
}
}
break;
@@ -796,7 +875,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
// Do not forward to input manager, the mouse will be captured
// on SDL_MOUSEBUTTONUP
return true;
return;
}
break;
case SDL_FINGERMOTION:
@@ -805,19 +884,18 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
if (relative_mode) {
// Touch events are not compatible with relative mode
// (coordinates are not relative)
return true;
return;
}
break;
case SDL_MOUSEBUTTONUP:
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
sc_screen_set_mouse_capture(screen, true);
return true;
return;
}
break;
}
sc_input_manager_handle_event(&screen->im, event);
return true;
}
struct sc_point

View File

@@ -9,7 +9,6 @@
#include "controller.h"
#include "coords.h"
#include "display.h"
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
@@ -25,7 +24,6 @@ struct sc_screen {
bool open; // track the open/close state to assert correct behavior
#endif
struct sc_display display;
struct sc_input_manager im;
struct sc_frame_buffer fb;
struct sc_fps_counter fps_counter;
@@ -41,6 +39,9 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@@ -56,6 +57,7 @@ struct sc_screen {
bool has_frame;
bool fullscreen;
bool maximized;
bool mipmaps;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
@@ -76,6 +78,7 @@ struct sc_screen_params {
const struct sc_shortcut_mods *shortcut_mods;
const char *window_title;
struct sc_size frame_size;
bool always_on_top;
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
@@ -133,9 +136,8 @@ void
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
// react to SDL events
// If this function returns false, scrcpy must exit with an error.
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
void
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels

View File

@@ -226,16 +226,12 @@ execute_server(struct sc_server *server,
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
if (!params->video) {
ADD_PARAM("video=false");
}
if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
}
if (!params->audio) {
ADD_PARAM("audio=false");
}
if (params->audio_bit_rate) {
} else if (params->audio_bit_rate) {
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
}
if (params->video_codec != SC_CODEC_H264) {
@@ -445,9 +441,9 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
static bool
device_read_info(struct sc_intr *intr, sc_socket device_socket,
struct sc_server_info *info) {
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH];
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4];
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) {
LOGE("Could not retrieve device information");
return false;
}
@@ -455,6 +451,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));
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;
}
@@ -467,7 +466,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial;
assert(serial);
bool video = server->params.video;
bool audio = server->params.audio;
bool control = server->params.control;
@@ -475,12 +473,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) {
if (video) {
video_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
if (audio) {
@@ -511,45 +506,35 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
unsigned attempts = 100;
sc_tick delay = SC_TICK_FROM_MS(100);
sc_socket first_socket = connect_to_server(server, attempts, delay,
tunnel_host, tunnel_port);
if (first_socket == SC_SOCKET_NONE) {
video_socket = connect_to_server(server, attempts, delay, tunnel_host,
tunnel_port);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
if (video) {
video_socket = first_socket;
}
if (audio) {
if (!video) {
audio_socket = first_socket;
} else {
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;
}
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) {
if (!video && !audio) {
control_socket = first_socket;
} else {
control_socket = net_socket();
if (control_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, control_socket,
tunnel_host, tunnel_port);
if (!ok) {
goto fail;
}
// we know that the device is listening, we don't need several
// attempts
control_socket = net_socket();
if (control_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, control_socket,
tunnel_host, tunnel_port);
if (!ok) {
goto fail;
}
}
}
@@ -558,17 +543,13 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
sc_socket first_socket = video ? video_socket
: audio ? audio_socket
: control_socket;
// The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, first_socket, info);
bool ok = device_read_info(&server->intr, video_socket, info);
if (!ok) {
goto fail;
}
assert(!video || video_socket != SC_SOCKET_NONE);
assert(video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE);
@@ -952,11 +933,8 @@ run_server(void *data) {
sc_mutex_unlock(&server->mutex);
// Interrupt sockets to wake up socket blocking calls on the server
if (server->video_socket != SC_SOCKET_NONE) {
// There is no video_socket if --no-video is set
net_interrupt(server->video_socket);
}
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

View File

@@ -18,6 +18,7 @@
#define SC_DEVICE_NAME_FIELD_LENGTH 64
struct sc_server_info {
char device_name[SC_DEVICE_NAME_FIELD_LENGTH];
struct sc_size frame_size;
};
struct sc_server_params {
@@ -41,7 +42,6 @@ struct sc_server_params {
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool video;
bool audio;
bool show_touches;
bool stay_awake;

View File

@@ -7,6 +7,8 @@
#include <stdbool.h>
#include <libavcodec/avcodec.h>
typedef struct AVFrame AVFrame;
/**
* Frame sink trait.
*
@@ -17,7 +19,6 @@ struct sc_frame_sink {
};
struct sc_frame_sink_ops {
/* The codec context is valid until the sink is closed */
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
void (*close)(struct sc_frame_sink *sink);
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);

View File

@@ -5,7 +5,9 @@
#include <assert.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
typedef struct AVCodec AVCodec;
typedef struct AVPacket AVPacket;
/**
* Packet sink trait.
@@ -17,8 +19,8 @@ struct sc_packet_sink {
};
struct sc_packet_sink_ops {
/* The codec context is valid until the sink is closed */
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
/* 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);

View File

@@ -25,11 +25,11 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx) {
const AVCodec *codec) {
assert(source->sink_count);
for (unsigned i = 0; i < source->sink_count; ++i) {
struct sc_packet_sink *sink = source->sinks[i];
if (!sink->ops->open(sink, ctx)) {
if (!sink->ops->open(sink, codec)) {
sc_packet_source_sinks_close_firsts(source, i);
return false;
}

View File

@@ -26,7 +26,7 @@ sc_packet_source_add_sink(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx);
const AVCodec *codec);
void
sc_packet_source_sinks_close(struct sc_packet_source *source);

View File

@@ -1,94 +0,0 @@
#ifndef SC_AUDIOBUF_H
#define SC_AUDIOBUF_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include "util/bytebuf.h"
/**
* Wrapper around bytebuf to read and write samples
*
* Each sample takes sample_size bytes.
*/
struct sc_audiobuf {
struct sc_bytebuf buf;
size_t sample_size;
};
static inline uint32_t
sc_audiobuf_to_samples(struct sc_audiobuf *buf, size_t bytes) {
assert(bytes % buf->sample_size == 0);
return bytes / buf->sample_size;
}
static inline size_t
sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) {
return samples * buf->sample_size;
}
static inline bool
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
uint32_t capacity) {
buf->sample_size = sample_size;
return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1);
}
static inline void
sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_read(&buf->buf, to, bytes);
}
static inline void
sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_skip(&buf->buf, bytes);
}
static inline void
sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from,
uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_write(&buf->buf, from, bytes);
}
static inline void
sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from,
uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_prepare_write(&buf->buf, from, bytes);
}
static inline void
sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) {
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
sc_bytebuf_commit_write(&buf->buf, bytes);
}
static inline uint32_t
sc_audiobuf_can_read(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_read(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline uint32_t
sc_audiobuf_can_write(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_can_write(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline uint32_t
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
size_t bytes = sc_bytebuf_capacity(&buf->buf);
return sc_audiobuf_to_samples(buf, bytes);
}
static inline void
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
sc_bytebuf_destroy(&buf->buf);
}
#endif

View File

@@ -30,7 +30,7 @@ sc_bytebuf_destroy(struct sc_bytebuf *buf) {
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(len <= sc_bytebuf_read_available(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
@@ -50,7 +50,7 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_read(buf));
assert(len <= sc_bytebuf_read_available(buf));
assert(buf->tail != buf->head); // the buffer could not be empty
buf->tail = (buf->tail + len) % buf->alloc_size;
@@ -78,7 +78,7 @@ sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
assert(len);
assert(len <= sc_bytebuf_can_write(buf));
assert(len <= sc_bytebuf_write_available(buf));
sc_bytebuf_write_step0(buf, from, len);
sc_bytebuf_write_step1(buf, len);
@@ -99,6 +99,6 @@ sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
assert(len <= sc_bytebuf_can_write(buf));
assert(len <= sc_bytebuf_write_available(buf));
sc_bytebuf_write_step1(buf, len);
}

View File

@@ -86,7 +86,7 @@ sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
* It is an error to read more bytes than available.
*/
static inline size_t
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
sc_bytebuf_read_available(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
}
@@ -96,12 +96,12 @@ sc_bytebuf_can_read(struct sc_bytebuf *buf) {
* It is an error to write more bytes than available.
*/
static inline size_t
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
sc_bytebuf_write_available(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
}
/**
* Return the actual capacity of the buffer (can_read() + can_write())
* Return the actual capacity of the buffer (read available + write available)
*/
static inline size_t
sc_bytebuf_capacity(struct sc_bytebuf *buf) {

View File

@@ -205,13 +205,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
goto error_avformat_free_context;
}
int r = avcodec_parameters_from_context(ostream->codecpar, ctx);
if (r < 0) {
goto error_avformat_free_context;
}
// The codec is from the v4l2 encoder, not from the decoder
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = encoder->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = vs->frame_size.width;
ostream->codecpar->height = vs->frame_size.height;
int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE);
if (ret < 0) {
@@ -226,8 +224,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
goto error_avio_close;
}
vs->encoder_ctx->width = ctx->width;
vs->encoder_ctx->height = ctx->height;
vs->encoder_ctx->width = vs->frame_size.width;
vs->encoder_ctx->height = vs->frame_size.height;
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
vs->encoder_ctx->time_base.num = 1;
vs->encoder_ctx->time_base.den = 1;
@@ -343,13 +341,16 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
}
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name) {
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct sc_size frame_size) {
vs->device_name = strdup(device_name);
if (!vs->device_name) {
LOGE("Could not strdup v4l2 device name");
return false;
}
vs->frame_size = frame_size;
static const struct sc_frame_sink_ops ops = {
.open = sc_v4l2_frame_sink_open,
.close = sc_v4l2_frame_sink_close,

View File

@@ -19,6 +19,7 @@ struct sc_v4l2_sink {
AVCodecContext *encoder_ctx;
char *device_name;
struct sc_size frame_size;
sc_thread thread;
sc_mutex mutex;
@@ -32,7 +33,8 @@ struct sc_v4l2_sink {
};
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name);
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct sc_size frame_size);
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);

View File

@@ -217,18 +217,6 @@ static void test_get_ip_multiline_second_ok(void) {
free(ip);
}
static void test_get_ip_multiline_second_ok_without_cr(void) {
char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.3\n"
"192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.3\n";
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(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34\r\r\n";
@@ -271,7 +259,6 @@ int main(int argc, char *argv[]) {
test_get_ip_single_line_with_trailing_space();
test_get_ip_multiline_first_ok();
test_get_ip_multiline_second_ok();
test_get_ip_multiline_second_ok_without_cr();
test_get_ip_no_wlan();
test_get_ip_no_wlan_without_eol();
test_get_ip_truncated();

View File

@@ -13,23 +13,23 @@ void test_bytebuf_simple(void) {
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
assert(sc_bytebuf_can_read(&buf) == 5);
assert(sc_bytebuf_read_available(&buf) == 5);
sc_bytebuf_read(&buf, data, 4);
assert(!strncmp((char *) data, "hell", 4));
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
assert(sc_bytebuf_can_read(&buf) == 7);
assert(sc_bytebuf_read_available(&buf) == 7);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 8);
assert(sc_bytebuf_read_available(&buf) == 8);
sc_bytebuf_read(&buf, &data[4], 8);
assert(sc_bytebuf_can_read(&buf) == 0);
assert(sc_bytebuf_read_available(&buf) == 0);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
assert(sc_bytebuf_read_available(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
@@ -42,31 +42,31 @@ void test_bytebuf_boundaries(void) {
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
assert(sc_bytebuf_read_available(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
assert(sc_bytebuf_read_available(&buf) == 12);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 18);
assert(sc_bytebuf_read_available(&buf) == 18);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 9));
assert(sc_bytebuf_can_read(&buf) == 9);
assert(sc_bytebuf_read_available(&buf) == 9);
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
assert(sc_bytebuf_read_available(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
assert(sc_bytebuf_read_available(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
assert(sc_bytebuf_read_available(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
assert(sc_bytebuf_read_available(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
@@ -79,37 +79,37 @@ void test_bytebuf_two_steps_write(void) {
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 6);
assert(sc_bytebuf_read_available(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12);
assert(sc_bytebuf_read_available(&buf) == 12);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 3));
assert(sc_bytebuf_can_read(&buf) == 3);
assert(sc_bytebuf_read_available(&buf) == 3);
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
assert(sc_bytebuf_can_read(&buf) == 9);
assert(sc_bytebuf_read_available(&buf) == 9);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
assert(sc_bytebuf_can_read(&buf) == 14);
assert(sc_bytebuf_read_available(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_can_read(&buf) == 15);
assert(sc_bytebuf_read_available(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_can_read(&buf) == 12);
assert(sc_bytebuf_read_available(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_can_read(&buf) == 0);
assert(sc_bytebuf_read_available(&buf) == 0);
sc_bytebuf_destroy(&buf);
}

View File

@@ -53,7 +53,7 @@ static void test_options(void) {
"--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-mirror" is not compatible with "--fulscreen"
// "--no-display" is not compatible with "--fulscreen"
"--port", "1234:1236",
"--push-target", "/sdcard/Movies",
"--record", "file",
@@ -108,8 +108,8 @@ static void test_options2(void) {
char *argv[] = {
"scrcpy",
"--no-control",
"--no-mirror",
"--record", "file.mp4", // cannot enable --no-mirror without recording
"--no-display",
"--record", "file.mp4", // cannot enable --no-display without recording
};
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
@@ -117,7 +117,7 @@ static void test_options2(void) {
const struct scrcpy_options *opts = &args.opts;
assert(!opts->control);
assert(!opts->mirror);
assert(!opts->display);
assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == SC_RECORD_FORMAT_MP4);
}

79
app/tests/test_clock.c Normal file
View File

@@ -0,0 +1,79 @@
#include "common.h"
#include <assert.h>
#include "clock.h"
void test_small_rolling_sum(void) {
struct sc_clock clock;
sc_clock_init(&clock);
assert(clock.count == 0);
assert(clock.left_sum.system == 0);
assert(clock.left_sum.stream == 0);
assert(clock.right_sum.system == 0);
assert(clock.right_sum.stream == 0);
sc_clock_update(&clock, 2, 3);
assert(clock.count == 1);
assert(clock.left_sum.system == 0);
assert(clock.left_sum.stream == 0);
assert(clock.right_sum.system == 2);
assert(clock.right_sum.stream == 3);
sc_clock_update(&clock, 10, 20);
assert(clock.count == 2);
assert(clock.left_sum.system == 2);
assert(clock.left_sum.stream == 3);
assert(clock.right_sum.system == 10);
assert(clock.right_sum.stream == 20);
sc_clock_update(&clock, 40, 80);
assert(clock.count == 3);
assert(clock.left_sum.system == 2);
assert(clock.left_sum.stream == 3);
assert(clock.right_sum.system == 50);
assert(clock.right_sum.stream == 100);
sc_clock_update(&clock, 400, 800);
assert(clock.count == 4);
assert(clock.left_sum.system == 12);
assert(clock.left_sum.stream == 23);
assert(clock.right_sum.system == 440);
assert(clock.right_sum.stream == 880);
}
void test_large_rolling_sum(void) {
const unsigned half_range = SC_CLOCK_RANGE / 2;
struct sc_clock clock1;
sc_clock_init(&clock1);
for (unsigned i = 0; i < 5 * half_range; ++i) {
sc_clock_update(&clock1, i, 2 * i + 1);
}
struct sc_clock clock2;
sc_clock_init(&clock2);
for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
sc_clock_update(&clock2, i, 2 * i + 1);
}
assert(clock1.count == SC_CLOCK_RANGE);
assert(clock2.count == SC_CLOCK_RANGE);
// The values before the last SC_CLOCK_RANGE points in clock1 should have
// no impact
assert(clock1.left_sum.system == clock2.left_sum.system);
assert(clock1.left_sum.stream == clock2.left_sum.stream);
assert(clock1.right_sum.system == clock2.right_sum.system);
assert(clock1.right_sum.stream == clock2.right_sum.stream);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_small_rolling_sum();
test_large_rolling_sum();
return 0;
};

View File

@@ -16,6 +16,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win32'
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@@ -16,6 +16,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-3/win64'
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@@ -1,116 +0,0 @@
# Audio
Audio forwarding is supported for devices with Android 11 or higher, and it is
enabled by default:
- For **Android 12 or newer**, it works out-of-the-box.
- For **Android 11**, you'll need to ensure that the device screen is unlocked
when starting scrcpy. A fake popup will briefly appear to make the system
think that the shell app is in the foreground. Without this, audio capture
will fail.
- For **Android 10 or earlier**, audio cannot be captured and is automatically
disabled.
If audio capture fails, then mirroring continues with video only (since audio is
enabled by default, it is not acceptable to make scrcpy fail if it is not
available), unless `--require-audio` is set.
## No audio
To disable audio:
```
scrcpy --no-audio
```
## Audio only
To play audio only, disable the video:
```
scrcpy --no-video
```
Without video, the audio latency is typically not criticial, so it might be
interesting to add [buffering](#buffering) to minimize glitches:
```
scrcpy --no-video --audio-buffer=200
```
## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac`
and `raw` (uncompressed PCM 16-bit LE):
```bash
scrcpy --audio-codec=opus # default
scrcpy --audio-codec=aac
scrcpy --audio-codec=raw
```
Several encoders may be available on the device. They can be listed by:
```bash
scrcpy --list-encoders
```
To select a specific encoder:
```
scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder'
```
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
## Bit rate
The default video bit-rate is 128Kbps. To change it:
```bash
scrcpy --audio-bit-rate=64K
scrcpy --audio-bit-rate=64000 # equivalent
```
_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._
## Buffering
Audio buffering is unavoidable. It must be kept small enough so that the latency
is acceptable, but large enough to minimize buffer underrun (causing audio
glitches).
The default buffer size is set to 50ms. It can be adjusted:
```bash
scrcpy --audio-buffer=40 # smaller than default
scrcpy --audio-buffer=100 # higher than default
```
Note that this option changes the _target_ buffering. It is possible that this
target buffering might not be reached (on frequent buffer underflow typically).
If you don't interact with the device (to watch a video for example), a higher
latency (for both [video](video.md#buffering) and audio) might be preferable to
avoid glitches and smooth the playback:
```
scrcpy --display-buffer=200 --audio-buffer=200
```
It is also possible to configure another audio buffer (the audio output buffer),
by default set to 5ms. Don't change it, unless you get some [robotic and glitchy
sound][#3793]:
```bash
# Only if absolutely necessary
scrcpy --audio-output-buffer=10
```
[#3793]: https://github.com/Genymobile/scrcpy/issues/3793

View File

@@ -1,149 +0,0 @@
# Control
## Read-only
To disable controls (everything which can interact with the device: input keys,
mouse events, drag&drop files):
```bash
scrcpy --no-control
scrcpy -n # short version
```
## Text injection preference
Two kinds of [events][textevents] are generated when typing text:
- _key events_, signaling that a key is pressed or released;
- _text events_, signaling that a text has been entered.
By default, letters are injected using key events, so that the keyboard behaves
as expected in games (typically for WASD keys).
But this may [cause issues][prefertext]. If you encounter such a problem, you
can avoid it by:
```bash
scrcpy --prefer-text
```
(but this will break keyboard behavior in games)
On the contrary, you could force to always inject raw key events:
```bash
scrcpy --raw-key-events
```
These options have no effect on HID keyboard (all key events are sent as
scancodes in this mode).
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
## Copy-paste
Any time the Android clipboard changes, it is automatically synchronized to the
computer clipboard.
Any <kbd>Ctrl</kbd> shortcut is forwarded to the device. In particular:
- <kbd>Ctrl</kbd>+<kbd>c</kbd> typically copies
- <kbd>Ctrl</kbd>+<kbd>x</kbd> typically cuts
- <kbd>Ctrl</kbd>+<kbd>v</kbd> typically pastes (after computer-to-device
clipboard synchronization)
This typically works as you expect.
The actual behavior depends on the active application though. For example,
_Termux_ sends SIGINT on <kbd>Ctrl</kbd>+<kbd>c</kbd> instead, and _K-9 Mail_
composes a new message.
To copy, cut and paste in such cases (but only supported on Android >= 7):
- <kbd>MOD</kbd>+<kbd>c</kbd> injects `COPY`
- <kbd>MOD</kbd>+<kbd>x</kbd> injects `CUT`
- <kbd>MOD</kbd>+<kbd>v</kbd> injects `PASTE` (after computer-to-device
clipboard synchronization)
In addition, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> injects the computer
clipboard text as a sequence of key events. This is useful when the component
does not accept text pasting (for example in _Termux_), but it can break
non-ASCII content.
**WARNING:** Pasting the computer clipboard to the device (either via
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) copies the content
into the Android clipboard. As a consequence, any Android application could read
its content. You should avoid pasting sensitive content (like passwords) that
way.
Some Android devices do not behave as expected when setting the device clipboard
programmatically. An option `--legacy-paste` is provided to change the behavior
of <kbd>Ctrl</kbd>+<kbd>v</kbd> and <kbd>MOD</kbd>+<kbd>v</kbd> so that they
also inject the computer clipboard text as a sequence of key events (the same
way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
To disable automatic clipboard synchronization, use
`--no-clipboard-autosync`.
## Pinch-to-zoom
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
Until the left-click button is released, all mouse movements scale and rotate
the content (if supported by the app) relative to the center of the screen.
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
at a location inverted through the center of the screen.
## Key repeat
By default, holding a key down generates repeated key events. This can cause
performance problems in some games, where these events are useless anyway.
To avoid forwarding repeated key events:
```bash
scrcpy --no-key-repeat
```
This option has no effect on HID keyboard (key repeat is handled by Android
directly in this mode).
## Right-click and middle-click
By default, right-click triggers BACK (or POWER on) and middle-click triggers
HOME. To disable these shortcuts and forward the clicks to the device instead:
```bash
scrcpy --forward-all-clicks
```
## File drop
### Install APK
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
window.
There is no visual feedback, a log is printed to the console.
### Push file to device
To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK)
file to the _scrcpy_ window.
There is no visual feedback, a log is printed to the console.
The target directory can be changed on start:
```bash
scrcpy --push-target=/sdcard/Movies/
```
## Physical keyboard and mouse simulation
See the dedicated [HID/OTG](hid-otg.md) page.

View File

@@ -1,228 +0,0 @@
# Device
## Selection
If exactly one device is connected (i.e. listed by `adb devices`), then it is
automatically selected.
However, if there are multiple devices connected, you must specify the one to
use in one of 4 ways:
- by its serial:
```bash
scrcpy --serial=0123456789abcdef
scrcpy -s 0123456789abcdef # short version
# the serial is the ip:port if connected over TCP/IP (same behavior as adb)
scrcpy --serial=192.168.1.1:5555
```
- the one connected over USB (if there is exactly one):
```bash
scrcpy --select-usb
scrcpy -d # short version
```
- the one connected over TCP/IP (if there is exactly one):
```bash
scrcpy --select-tcpip
scrcpy -e # short version
```
- a device already listening on TCP/IP (see [below](#tcpip-wireless)):
```bash
scrcpy --tcpip=192.168.1.1:5555
scrcpy --tcpip=192.168.1.1 # default port is 5555
```
The serial may also be provided via the environment variable `ANDROID_SERIAL`
(also used by `adb`):
```bash
# in bash
export ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```cmd
:: in cmd
set ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```powershell
# in PowerShell
$env:ANDROID_SERIAL = '0123456789abcdef'
scrcpy
```
## TCP/IP (wireless)
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP. The device must be connected on the same network as the
computer.
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### Automatic
An option `--tcpip` allows to configure the connection automatically. There are
two variants.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming _adb_ connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
address), connect the device over USB, then run:
```bash
scrcpy --tcpip # without arguments
```
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
Alternatively, it is possible to enable the TCP/IP connection manually using
`adb`:
1. Plug the device into a USB port on your computer.
2. Connect the device to the same Wi-Fi network as your computer.
3. Get your device IP address, in Settings → About phone → Status, or by
executing this command:
```bash
adb shell ip route | awk '{print $9}'
```
4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`.
5. Unplug your device.
6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`
with the device IP address you found)_.
7. Run `scrcpy` as usual.
8. Run `adb disconnect` once you're done.
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
having to physically connect your device directly to your computer.
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
## Autostart
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
```bash
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/autoadb
## Display
If several displays are available on the Android device, it is possible to
select the display to mirror:
```bash
scrcpy --display=1
```
The list of display ids can be retrieved by:
```bash
scrcpy --list-displays
```
A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only).
## Actions
Some command line arguments perform actions on the device itself while scrcpy is
running.
### Stay awake
To prevent the device from sleeping after a delay **when the device is plugged
in**:
```bash
scrcpy --stay-awake
scrcpy -w
```
The initial state is restored when _scrcpy_ is closed.
If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior).
### Turn screen off
It is possible to turn the device screen off while mirroring on start with a
command-line option:
```bash
scrcpy --turn-screen-off
scrcpy -S # short version
```
Or by pressing <kbd>MOD</kbd>+<kbd>o</kbd> at any time (see
[shortcuts](shortcuts.md)).
To turn it back on, press <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
On Android, the `POWER` button always turns the screen on. For convenience, if
`POWER` is sent via _scrcpy_ (via right-click or <kbd>MOD</kbd>+<kbd>p</kbd>),
it will force to turn the screen off after a small delay (on a best effort
basis). The physical `POWER` button will still cause the screen to be turned on.
It can also be useful to prevent the device from sleeping:
```bash
scrcpy --turn-screen-off --stay-awake
scrcpy -Sw # short version
```
### Show touches
For presentations, it may be useful to show physical touches (on the physical
device). Android exposes this feature in _Developers options_.
_Scrcpy_ provides an option to enable this feature on start and restore the
initial value on exit:
```bash
scrcpy --show-touches
scrcpy -t # short version
```
Note that it only shows _physical_ touches (by a finger on the device).
### Power off on close
To turn the device screen off when closing _scrcpy_:
```bash
scrcpy --power-off-on-close
```
### Power on on start
By default, on start, the device is powered on. To prevent this behavior:
```bash
scrcpy --no-power-on
```

View File

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

View File

@@ -1,81 +0,0 @@
# On Linux
## Install
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
Scrcpy is packaged in several distributions and package managers:
- Debian/Ubuntu: `apt install scrcpy`
- Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: [ebuild][ebuild-link] file
- Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions))
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
### Latest version
However, the packaged version is not always the latest release. To install the
latest release from `master`, follow this simplified process.
First, you need to install the required packages:
```bash
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
```
Then clone the repo and execute the installation script
([source](/install_release.sh)):
```bash
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
./install_release.sh
```
When a new release is out, update the repo and reinstall:
```bash
git pull
./install_release.sh
```
To uninstall:
```bash
sudo ninja -Cbuild-auto uninstall
```
_Note that this simplified process only works for released versions (it
downloads a prebuilt server binary), so for example you can't use it for testing
the development branch (`dev`)._
_See [build.md](build.md) to build and install the app manually._
## Run
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
Once installed, run from a terminal:
```bash
scrcpy
```
or with arguments (here to disable audio and record to `file.mkv`):
```bash
scrcpy --no-audio --record=file.mkv
```
Documentation for command line arguments is available:
- `man scrcpy`
- `scrcpy --help`
- on [github](/README.md)

View File

@@ -1,49 +0,0 @@
# On macOS
## Install
Scrcpy is available in [Homebrew]:
```bash
brew install scrcpy
```
[Homebrew]: https://brew.sh/
You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash
brew install android-platform-tools
```
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:
```bash
sudo port install scrcpy
```
[MacPorts]: https://www.macports.org/
_See [build.md](build.md) to build and install the app manually._
## Run
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
Once installed, run from a terminal:
```bash
scrcpy
```
or with arguments (here to disable audio and record to `file.mkv`):
```bash
scrcpy --no-audio --record=file.mkv
```
Documentation for command line arguments is available:
- `man scrcpy`
- `scrcpy --help`
- on [github](/README.md)

View File

@@ -1,50 +0,0 @@
# Recording
To record video and audio streams while mirroring:
```bash
scrcpy --record=file.mp4
scrcpy -r file.mkv
```
To record only the video:
```bash
scrcpy --no-audio --record=file.mp4
```
To record only the audio:
```bash
scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record-file=file.aac
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
```
To disable mirroring while recording:
```bash
scrcpy --no-mirror --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
Timestamps are captured on the device, so [packet delay variation] does not
impact the recorded file, which is always clean (only if you use `--record` of
course, not if you capture your scrcpy window and audio output on the computer).
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
The video and audio streams are encoded on the device, but are muxed on the
client side. Two formats (containers) are supported:
- Matroska (`.mkv`)
- MP4 (`.mp4`)
The container is automatically selected based on the filename.
It is also possible to explicitly select a container (in that case the filename
needs not end with `.mkv` or `.mp4`):
```
scrcpy --record=file --record-format=mkv
```

View File

@@ -1,68 +0,0 @@
# Shortcuts
Actions can be performed on the scrcpy window using keyboard and mouse
shortcuts.
In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's
(left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>.
It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
`lalt`, `ralt`, `lsuper` and `rsuper`. For example:
```bash
# use RCtrl for shortcuts
scrcpy --shortcut-mod=rctrl
# use either LCtrl+LAlt or LSuper for shortcuts
scrcpy --shortcut-mod=lctrl+lalt,lsuper
```
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
| Action | Shortcut
| ------------------------------------------- |:-----------------------------
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
| Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
| Click on `MENU` (unlock screen)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd></kbd> _(up)_
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd></kbd> _(down)_
| Click on `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
| Power on | _Right-click²_
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard⁵ | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
_³4th and 5th mouse buttons, if your mouse has them._
_⁴For react-native apps in development, `MENU` triggers development menu._
_⁵Only on Android >= 7._
Shortcuts with repeated keys are executed by releasing and pressing the key a
second time. For example, to execute "Expand settings panel":
1. Press and keep pressing <kbd>MOD</kbd>.
2. Then double-press <kbd>n</kbd>.
3. Finally, release <kbd>MOD</kbd>.
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application.

View File

@@ -1,123 +0,0 @@
# Tunnels
Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to
a remote device (e.g. over the Internet).
To connect to a remote device, it is possible to connect a local `adb` client to
a remote `adb` server (provided they use the same version of the _adb_
protocol).
## Remote ADB server
To connect to a remote _adb server_, make the server listen on all interfaces:
```bash
adb kill-server
adb -a nodaemon server start
# keep this open
```
**Warning: all communications between clients and the _adb server_ are
unencrypted.**
Suppose that this server is accessible at 192.168.1.2. Then, from another
terminal, run `scrcpy`:
```bash
# in bash
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
scrcpy --tunnel-host=192.168.1.2
```
```cmd
:: in cmd
set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
scrcpy --tunnel-host=192.168.1.2
```
```powershell
# in PowerShell
$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037'
scrcpy --tunnel-host=192.168.1.2
```
By default, `scrcpy` uses the local port used for `adb forward` tunnel
establishment (typically `27183`, see `--port`). It is also possible to force a
different tunnel port (it may be useful in more complex situations, when more
redirections are involved):
```
scrcpy --tunnel-port=1234
```
## SSH tunnel
To communicate with a remote _adb server_ securely, it is preferable to use an
SSH tunnel.
First, make sure the _adb server_ is running on the remote computer:
```bash
adb start-server
```
Then, establish an SSH tunnel:
```bash
# local 5038 --> remote 5037
# local 27183 <-- remote 27183
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal, run `scrcpy`:
```bash
# in bash
export ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy
```
```cmd
:: in cmd
set ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy
```
```powershell
# in PowerShell
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
scrcpy
```
To avoid enabling remote port forwarding, you could force a forward connection
instead (notice the `-L` instead of `-R`):
```bash
# local 5038 --> remote 5037
# local 27183 --> remote 27183
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
# keep this open
```
From another terminal, run `scrcpy`:
```bash
# in bash
export ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy --force-adb-forward
```
```cmd
:: in cmd
set ADB_SERVER_SOCKET=tcp:localhost:5038
scrcpy --force-adb-forward
```
```powershell
# in PowerShell
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
scrcpy --force-adb-forward
```

View File

@@ -1,65 +0,0 @@
# Video4Linux
On Linux, it is possible to send the video stream to a [v4l2] loopback device,
so that the Android device can be opened like a webcam by any v4l2-capable tool.
[v4l2]: https://en.wikipedia.org/wiki/Video4Linux
The module `v4l2loopback` must be installed:
```bash
sudo apt install v4l2loopback-dkms
```
To create a v4l2 device:
```bash
sudo modprobe v4l2loopback
```
This will create a new video device in `/dev/videoN`, where `N` is an integer
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
to create several devices or devices with specific IDs).
To list the enabled devices:
```bash
# requires v4l-utils package
v4l2-ctl --list-devices
# simple but might be sufficient
ls /dev/video*
```
To start `scrcpy` using a v4l2 sink:
```bash
scrcpy --v4l2-sink=/dev/videoN
scrcpy --v4l2-sink=/dev/videoN --no-mirror # disable mirroring window
```
(replace `N` with the device ID, check with `ls /dev/video*`)
Once enabled, you can open your video stream with a v4l2-capable tool:
```bash
ffplay -i /dev/videoN
vlc v4l2:///dev/videoN # VLC might add some buffering delay
```
For example, you could capture the video within [OBS] or within your video
conference tool.
[OBS]: https://obsproject.com/
## Buffering
By default, there is no video buffering, to get the lowest possible latency.
As for the [video display](video.md#buffering), it is possible to add
buffering to delay the v4l2 stream:
```bash
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
```

View File

@@ -1,185 +0,0 @@
# Video
## Size
By default, scrcpy attempts to mirror at the Android device resolution.
It might be useful to mirror at a lower definition to increase performance. To
limit both width and height to some maximum value (here 1024):
```bash
scrcpy --max-size=1024
scrcpy -m 1024 # short version
```
The other dimension is computed so that the Android device aspect ratio is
preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
If encoding fails, scrcpy automatically tries again with a lower definition
(unless `--no-downsize-on-error` is enabled).
## Bit rate
The default video bit-rate is 8 Mbps. To change it:
```bash
scrcpy --video-bit-rate=2M
scrcpy --video-bit-rate=2000000 # equivalent
scrcpy -b 2M # short version
```
## Frame rate
The capture frame rate can be limited:
```bash
scrcpy --max-fps=15
```
The actual capture frame rate may be printed to the console:
```
scrcpy --print-fps
```
It may also be enabled or disabled at anytime with <kbd>MOD</kbd>+<kbd>i</kbd>
(see [shortcuts](shortcuts.md)).
The frame rate is intrinsically variable: a new frame is produced only when the
screen content changes. For example, if you play a fullscreen video at 24fps on
your device, you should not get more than 24 frames per second in scrcpy.
## Codec
The video codec can be selected. The possible values are `h264` (default),
`h265` and `av1`:
```bash
scrcpy --video-codec=h264 # default
scrcpy --video-codec=h265
scrcpy --video-codec=av1
```
H265 may provide better quality, but H264 should provide lower latency.
AV1 encoders are not common on current Android devices.
Several encoders may be available on the device. They can be listed by:
```bash
scrcpy --list-encoders
```
Sometimes, the default encoder may have issues or even crash, so it is useful to
try another one:
```bash
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
```
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--video-codec-options` in the manpage or in `scrcpy --help`.
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
## Rotation
The rotation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the
recording.
- `--rotation` rotates only the window content. This only affects the display,
not the recording. It may be changed dynamically at any time using the
[shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd></kbd> and
<kbd>MOD</kbd>+<kbd></kbd>.
To lock the mirroring orientation:
```bash
scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --lock-video-orientation=1 # 90° counterclockwise
scrcpy --lock-video-orientation=2 # 180°
scrcpy --lock-video-orientation=3 # 90° clockwise
```
To set an initial window rotation:
```bash
scrcpy --rotation=0 # no rotation
scrcpy --rotation=1 # 90 degrees counterclockwise
scrcpy --rotation=2 # 180 degrees
scrcpy --rotation=3 # 90 degrees clockwise
```
## Crop
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)
```
The values are expressed in the device natural orientation (portrait for a
phone, landscape for a tablet).
If `--max-size` is also specified, resizing is applied after cropping.
## Buffering
By default, there is no video buffering, to get the lowest possible latency.
Buffering can be added to delay the video stream and compensate for jitter to
get a smoother playback (see [#2464]).
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
The configuration is available independently for the display,
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
```bash
scrcpy --display-buffer=50 # add 50ms buffering for display
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
```
They can be applied simultaneously:
```bash
scrcpy --display-buffer=50 --v4l2-buffer=300
```
## No mirror
It is possible to capture an Android device without displaying a mirroring
window. This option is available if either [recording](recording.md) or
[v4l2](#video4linux) is enabled:
```bash
scrcpy --v4l2-sink=/dev/video2 --no-mirror
scrcpy --record=file.mkv --no-mirror
```
## No video
To disable video forwarding completely, so that only audio is forwarded:
```
scrcpy --no-video
```
## Video4Linux
See the dedicated [Video4Linux](v4l2.md) page.

View File

@@ -1,55 +0,0 @@
# Window
## Title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title='My device'
```
## Position and size
The initial window position and size may be specified:
```bash
scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
```
## Borderless
To disable window decorations:
```bash
scrcpy --window-borderless
```
## Always on top
To keep the window always on top:
```bash
scrcpy --always-on-top
```
## Fullscreen
The app may be started directly in fullscreen:
```bash
scrcpy --fullscreen
scrcpy -f # short version
```
Fullscreen mode can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>
(see [shortcuts](shortcuts.md)).
## Disable screensaver
By default, _scrcpy_ does not prevent the screensaver from running on the
computer. To disable it:
```bash
scrcpy --disable-screensaver
```

View File

@@ -1,89 +0,0 @@
# On Windows
## Install
Download the [latest release]:
- [`scrcpy-win64-v2.0.zip`][direct-win64] (64-bit)
<sub>SHA-256: `ae4c8d37a496b43f8974ba8f07f708e22a9570ba0cddc3dc3a36edbccd4d2a20`</sub>
- [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit)
<sub>SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip
and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]:
```bash
choco install scrcpy
choco install adb # if you don't have it yet
```
or [Scoop]:
```bash
scoop install scrcpy
scoop install adb # if you don't have it yet
```
[Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh
_See [build.md](build.md) to build and install the app manually._
## Run
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
Scrcpy is a command line application: it is mainly intended to be executed from
a terminal with command line arguments.
To open a terminal at the expected location, double-click on
`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For
example, without arguments:
```bash
scrcpy
```
or with arguments (here to disable audio and record to `file.mkv`):
```
scrcpy --no-audio --record=file.mkv
```
Documentation for command line arguments is available:
- `scrcpy --help`
- on [github](/README.md)
To start scrcpy directly without opening a terminal, double-click on one of
these files:
- `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy
terminates, unless an error occurs);
- `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error
message).
_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would
close immediately and you won't have time to read any error message (this
executable is intended to be run from the terminal). Use `scrcpy-console.bat`
instead._
If you plan to always use the same arguments, create a file `myscrcpy.bat`
(enable [show file extensions] to avoid confusion) containing your command, For
example:
```bash
scrcpy --prefer-text --turn-screen-off --stay-awake
```
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
Then just double-click on that file.
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
to add some arguments.

View File

@@ -2,8 +2,8 @@
set -e
BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0
PREBUILT_SERVER_SHA256=9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2
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

View File

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

View File

@@ -94,15 +94,15 @@ 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-6.0-scrcpy-3/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/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
@@ -113,15 +113,15 @@ 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-6.0-scrcpy-3/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-3/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/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

View File

@@ -7,8 +7,8 @@ android {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 33
versionCode 20000
versionName "2.0"
versionCode 12500
versionName "1.25"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@@ -12,7 +12,7 @@
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.0
SCRCPY_VERSION_NAME=1.25
PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}

View File

@@ -1,16 +1,7 @@
package com.genymobile.scrcpy;
public interface AsyncProcessor {
interface TerminationListener {
/**
* Notify processor termination
*
* @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server.
*/
void onTerminated(boolean fatalError);
}
void start(TerminationListener listener);
void start();
void stop();
void join() throws InterruptedException;
}

View File

@@ -14,6 +14,7 @@ import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class AudioCapture {
@@ -59,58 +60,45 @@ public final class AudioCapture {
}
private static void startWorkaroundAndroid11() {
// 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.
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);
}
private static void stopWorkaroundAndroid11() {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
try {
startRecording();
return; // it worked
} catch (UnsupportedOperationException e) {
if (attempts == 0) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
throw new AudioCaptureForegroundException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
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 void startRecording() {
recorder = createAudioRecord();
recorder.startRecording();
private static void stopWorkaroundAndroid11() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
}
public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
startWorkaroundAndroid11();
try {
tryStartRecording(3, 100);
} finally {
stopWorkaroundAndroid11();
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 AudioCaptureForegroundException();
}
} else {
startRecording();
throw e;
} finally {
stopWorkaroundAndroid11();
}
}
@@ -122,9 +110,9 @@ public final class AudioCapture {
}
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) throws IOException {
int r = recorder.read(directBuffer, size);
if (r <= 0) {
if (r < 0) {
return r;
}
@@ -141,8 +129,8 @@ public final class AudioCapture {
pts = nextPts;
}
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs;
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
nextPts = pts + durationMs;
if (previousPts != 0 && pts < previousPts) {
// Audio PTS may come from two sources:

View File

@@ -92,7 +92,7 @@ public final class AudioEncoder implements AsyncProcessor {
InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = capture.read(buffer, READ_SIZE, bufferInfo);
if (r <= 0) {
if (r < 0) {
throw new IOException("Could not read audio: " + r);
}
@@ -101,7 +101,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
streamer.writeAudioHeader();
streamer.writeHeader();
while (!Thread.currentThread().isInterrupted()) {
OutputTask task = outputTasks.take();
@@ -114,29 +114,21 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
boolean fatalError = false;
try {
encode();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
} catch (ConfigurationException | AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
fatalError = true;
} finally {
Ln.d("Audio encoder stopped");
listener.onTerminated(fatalError);
}
});
thread.start();
}
@Override
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
@@ -144,7 +136,6 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
@@ -280,22 +271,13 @@ public final class AudioEncoder implements AsyncProcessor {
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
} catch (IOException e) {
Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
throw e;
}
}
try {
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
} catch (IOException | IllegalArgumentException e) {
Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
throw e;
}
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
}
private class EncoderCallback extends MediaCodec.Callback {

View File

@@ -1,7 +1,6 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import android.os.Build;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -20,12 +19,6 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
private void record() throws IOException, AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@@ -33,7 +26,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
try {
capture.start();
streamer.writeAudioHeader();
streamer.writeHeader();
while (!Thread.currentThread().isInterrupted()) {
buffer.position(0);
int r = capture.read(buffer, READ_SIZE, bufferInfo);
@@ -53,33 +46,27 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
boolean fatalError = false;
try {
record();
} catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio recording error", e);
fatalError = true;
} finally {
Ln.d("Audio recorder stopped");
listener.onTerminated(fatalError);
}
});
thread.start();
}
@Override
public void stop() {
if (thread != null) {
thread.interrupt();
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();

View File

@@ -84,8 +84,7 @@ public class Controller implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
try {
control();
@@ -93,14 +92,12 @@ public class Controller implements AsyncProcessor {
// this is expected on close
} finally {
Ln.d("Controller stopped");
listener.onTerminated(true);
}
});
thread.start();
sender.start();
}
@Override
public void stop() {
if (thread != null) {
thread.interrupt();
@@ -108,7 +105,6 @@ public class Controller implements AsyncProcessor {
sender.stop();
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();

View File

@@ -41,7 +41,7 @@ public final class DesktopConnection implements Closeable {
controlInputStream = null;
controlOutputStream = null;
}
videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
videoFd = videoSocket.getFileDescriptor();
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
}
@@ -60,43 +60,29 @@ public final class DesktopConnection implements Closeable {
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
}
public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte)
throws IOException {
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(scid);
LocalSocket firstSocket = null;
LocalSocket videoSocket = null;
LocalSocket audioSocket = null;
LocalSocket controlSocket = null;
try {
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
if (video) {
videoSocket = localServerSocket.accept();
firstSocket = videoSocket;
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (audio) {
audioSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = audioSocket;
}
}
if (control) {
controlSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = controlSocket;
}
}
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
firstSocket.getOutputStream().write(0);
}
}
} else {
if (video) {
videoSocket = connect(socketName);
}
videoSocket = connect(socketName);
if (audio) {
audioSocket = connect(socketName);
}
@@ -120,22 +106,10 @@ public final class DesktopConnection implements Closeable {
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
}
private LocalSocket getFirstSocket() {
if (videoSocket != null) {
return videoSocket;
}
if (audioSocket != null) {
return audioSocket;
}
return controlSocket;
}
public void close() throws IOException {
if (videoSocket != null) {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
}
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
if (audioSocket != null) {
audioSocket.shutdownInput();
audioSocket.shutdownOutput();
@@ -148,16 +122,19 @@ public final class DesktopConnection implements Closeable {
}
}
public void sendDeviceMeta(String deviceName) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH];
public void sendDeviceMeta(String deviceName, int width, int height) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
FileDescriptor fd = getFirstSocket().getFileDescriptor();
IO.writeFully(fd, buffer, 0, buffer.length);
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(videoFd, buffer, 0, buffer.length);
}
public FileDescriptor getVideoFd() {

View File

@@ -288,7 +288,10 @@ public final class Device {
boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
}
return allOk;
}

View File

@@ -38,10 +38,4 @@ public final class FakeContext extends ContextWrapper {
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
// @Override to be added on SDK upgrade for Android 14
@SuppressWarnings("unused")
public int getDeviceId() {
return 0;
}
}

View File

@@ -3,19 +3,17 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.List;
import java.util.Locale;
public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int scid = -1; // 31-bit non-negative value, or -1
private boolean video = true;
private boolean audio = true;
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int audioBitRate = 196000;
private int maxFps;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
@@ -42,92 +40,172 @@ public class Options {
private boolean sendDeviceMeta = true; // send device name and size
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
private boolean sendCodecMeta = true; // write the codec metadata before the stream
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
public Ln.Level getLogLevel() {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getScid() {
return scid;
}
public boolean getVideo() {
return video;
public void setScid(int scid) {
this.scid = scid;
}
public boolean getAudio() {
return audio;
}
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public VideoCodec getVideoCodec() {
return videoCodec;
}
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() {
return audioCodec;
}
public void setAudioCodec(AudioCodec audioCodec) {
this.audioCodec = audioCodec;
}
public int getVideoBitRate() {
return videoBitRate;
}
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() {
return audioBitRate;
}
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
}
public int getMaxFps() {
return maxFps;
}
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getControl() {
return control;
}
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() {
return showTouches;
}
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() {
return stayAwake;
}
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
}
public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions;
}
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() {
return videoEncoder;
}
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() {
return audioEncoder;
}
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose;
}
@@ -136,207 +214,79 @@ public class Options {
return clipboardAutosync;
}
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
public boolean getDownsizeOnError() {
return downsizeOnError;
}
public void setDownsizeOnError(boolean downsizeOnError) {
this.downsizeOnError = downsizeOnError;
}
public boolean getCleanup() {
return cleanup;
}
public void setCleanup(boolean cleanup) {
this.cleanup = cleanup;
}
public boolean getPowerOn() {
return powerOn;
}
public void setPowerOn(boolean powerOn) {
this.powerOn = powerOn;
}
public boolean getListEncoders() {
return listEncoders;
}
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() {
return listDisplays;
}
public void setListDisplays(boolean listDisplays) {
this.listDisplays = listDisplays;
}
public boolean getSendDeviceMeta() {
return sendDeviceMeta;
}
public void setSendDeviceMeta(boolean sendDeviceMeta) {
this.sendDeviceMeta = sendDeviceMeta;
}
public boolean getSendFrameMeta() {
return sendFrameMeta;
}
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getSendDummyByte() {
return sendDummyByte;
}
public boolean getSendCodecMeta() {
return sendCodecMeta;
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
@SuppressWarnings("MethodLength")
public static Options parse(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.scid = scid;
break;
case "log_level":
options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
break;
case "video":
options.video = Boolean.parseBoolean(value);
break;
case "audio":
options.audio = Boolean.parseBoolean(value);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.videoCodec = videoCodec;
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.audioCodec = audioCodec;
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;
case "video_bit_rate":
options.videoBitRate = Integer.parseInt(value);
break;
case "audio_bit_rate":
options.audioBitRate = Integer.parseInt(value);
break;
case "max_fps":
options.maxFps = Integer.parseInt(value);
break;
case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value);
break;
case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value);
break;
case "crop":
options.crop = parseCrop(value);
break;
case "control":
options.control = Boolean.parseBoolean(value);
break;
case "display_id":
options.displayId = Integer.parseInt(value);
break;
case "show_touches":
options.showTouches = Boolean.parseBoolean(value);
break;
case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value);
break;
case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value);
break;
case "audio_codec_options":
options.audioCodecOptions = CodecOption.parse(value);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.videoEncoder = value;
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.audioEncoder = value;
}
case "power_off_on_close":
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
break;
case "clipboard_autosync":
options.clipboardAutosync = Boolean.parseBoolean(value);
break;
case "downsize_on_error":
options.downsizeOnError = Boolean.parseBoolean(value);
break;
case "cleanup":
options.cleanup = Boolean.parseBoolean(value);
break;
case "power_on":
options.powerOn = Boolean.parseBoolean(value);
break;
case "list_encoders":
options.listEncoders = Boolean.parseBoolean(value);
break;
case "list_displays":
options.listDisplays = Boolean.parseBoolean(value);
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;
case "send_frame_meta":
options.sendFrameMeta = Boolean.parseBoolean(value);
break;
case "send_dummy_byte":
options.sendDummyByte = Boolean.parseBoolean(value);
break;
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendCodecMeta = false;
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
public boolean getSendCodecId() {
return sendCodecId;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
}

View File

@@ -16,7 +16,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@@ -39,9 +39,6 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
private boolean firstFrameSent;
private int consecutiveErrors;
private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean();
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) {
this.device = device;
@@ -58,18 +55,18 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
rotationChanged.set(true);
}
private boolean consumeRotationChange() {
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException {
public void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
streamer.writeHeader();
boolean alive;
try {
@@ -166,14 +163,9 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (consumeRotationChange()) {
@@ -201,7 +193,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
}
}
return !eof && alive;
return !eof;
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@@ -210,22 +202,13 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
} catch (IOException e) {
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
throw e;
}
}
try {
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
} catch (IOException | IllegalArgumentException e) {
Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
throw e;
}
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
}
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
@@ -275,38 +258,4 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
SurfaceControl.closeTransaction();
}
}
@Override
public void start(TerminationListener listener) {
thread = new Thread(() -> {
try {
streamScreen();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
}
} finally {
Ln.d("Screen streaming stopped");
listener.onTerminated(true);
}
});
thread.start();
}
@Override
public void stop() {
if (thread != null) {
stopped.set(true);
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
}

View File

@@ -1,43 +1,16 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.os.BatteryManager;
import android.os.Build;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class Server {
private static class Completion {
private int running;
private boolean fatalError;
Completion(int running) {
this.running = running;
}
synchronized void addCompleted(boolean fatalError) {
--running;
if (fatalError) {
this.fatalError = true;
}
if (running == 0 || this.fatalError) {
notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
}
}
}
private Server() {
// not instantiable
}
@@ -95,7 +68,6 @@ public final class Server {
int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
@@ -109,23 +81,23 @@ public final class Server {
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
if (Build.BRAND.equalsIgnoreCase("meizu")) {
Workarounds.fillAppInfo();
}
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill the application context for the AudioRecord to work.
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Workarounds.fillAppContext();
// Only on Android 11 we must fill app info for the AudioRecord to work.
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
if (mustFillAppInfo) {
Workarounds.fillAppInfo();
}
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
try {
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) {
connection.sendDeviceMeta(Device.getDeviceName());
Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
}
if (control) {
@@ -136,7 +108,7 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(),
options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
@@ -148,23 +120,26 @@ public final class Server {
asyncProcessors.add(audioRecorder);
}
if (video) {
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(screenEncoder);
}
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
Completion completion = new Completion(asyncProcessors.size());
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.start((fatalError) -> {
completion.addCompleted(fatalError);
});
asyncProcessor.start();
}
completion.await();
try {
// synchronous
screenEncoder.streamScreen();
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
}
}
} finally {
Ln.d("Screen streaming stopped");
initThread.interrupt();
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop();
@@ -178,8 +153,6 @@ public final class Server {
} catch (InterruptedException e) {
// ignore
}
connection.close();
}
}
@@ -189,12 +162,203 @@ public final class Server {
return thread;
}
@SuppressWarnings("MethodLength")
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.setScid(scid);
break;
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "audio":
boolean audio = Boolean.parseBoolean(value);
options.setAudio(audio);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.setVideoCodec(videoCodec);
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.setAudioCodec(audioCodec);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "video_bit_rate":
int videoBitRate = Integer.parseInt(value);
options.setVideoBitRate(videoBitRate);
break;
case "audio_bit_rate":
int audioBitRate = Integer.parseInt(value);
options.setAudioBitRate(audioBitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockVideoOrientation = Integer.parseInt(value);
options.setLockVideoOrientation(lockVideoOrientation);
break;
case "tunnel_forward":
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "video_codec_options":
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
options.setVideoCodecOptions(videoCodecOptions);
break;
case "audio_codec_options":
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
options.setAudioCodecOptions(audioCodecOptions);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.setVideoEncoder(value);
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.setAudioEncoder(value);
}
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
break;
case "clipboard_autosync":
boolean clipboardAutosync = Boolean.parseBoolean(value);
options.setClipboardAutosync(clipboardAutosync);
break;
case "downsize_on_error":
boolean downsizeOnError = Boolean.parseBoolean(value);
options.setDownsizeOnError(downsizeOnError);
break;
case "cleanup":
boolean cleanup = Boolean.parseBoolean(value);
options.setCleanup(cleanup);
break;
case "power_on":
boolean powerOn = Boolean.parseBoolean(value);
options.setPowerOn(powerOn);
break;
case "list_encoders":
boolean listEncoders = Boolean.parseBoolean(value);
options.setListEncoders(listEncoders);
break;
case "list_displays":
boolean listDisplays = Boolean.parseBoolean(value);
options.setListDisplays(listDisplays);
break;
case "send_device_meta":
boolean sendDeviceMeta = Boolean.parseBoolean(value);
options.setSendDeviceMeta(sendDeviceMeta);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "send_dummy_byte":
boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte);
break;
case "send_codec_id":
boolean sendCodecId = Boolean.parseBoolean(value);
options.setSendCodecId(sendCodecId);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.setSendDeviceMeta(false);
options.setSendFrameMeta(false);
options.setSendDummyByte(false);
options.setSendCodecId(false);
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
});
Options options = Options.parse(args);
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());

View File

@@ -15,23 +15,24 @@ public final class Streamer {
private final FileDescriptor fd;
private final Codec codec;
private final boolean sendCodecMeta;
private final boolean sendCodecId;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
this.fd = fd;
this.codec = codec;
this.sendCodecMeta = sendCodecMeta;
this.sendCodecId = sendCodecId;
this.sendFrameMeta = sendFrameMeta;
}
public Codec getCodec() {
return codec;
}
public void writeAudioHeader() throws IOException {
if (sendCodecMeta) {
public void writeHeader() throws IOException {
if (sendCodecId) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
buffer.flip();
@@ -39,17 +40,6 @@ public final class Streamer {
}
}
public void writeVideoHeader(Size videoSize) throws IOException {
if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(12);
buffer.putInt(codec.getId());
buffer.putInt(videoSize.getWidth());
buffer.putInt(videoSize.getHeight());
buffer.flip();
IO.writeFully(fd, buffer);
}
}
public void writeDisableStream(boolean error) throws IOException {
// Writing a specific code as codec-id means that the device disables the stream
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only

View File

@@ -10,10 +10,6 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public final class Workarounds {
private static Class<?> activityThreadClass;
private static Object activityThread;
private Workarounds() {
// not instantiable
}
@@ -32,25 +28,18 @@ public final class Workarounds {
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
if (activityThread == null) {
public static void fillAppInfo() {
try {
// ActivityThread activityThread = new ActivityThread();
activityThreadClass = Class.forName("android.app.ActivityThread");
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
Object activityThread = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
}
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public static void fillAppInfo() {
try {
fillActivityThread();
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
@@ -70,16 +59,6 @@ public final class Workarounds {
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.d("Could not fill app info: " + throwable.getMessage());
}
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public static void fillAppContext() {
try {
fillActivityThread();
Application app = Application.class.newInstance();
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
@@ -92,7 +71,7 @@ public final class Workarounds {
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.d("Could not fill app context: " + throwable.getMessage());
Ln.d("Could not fill app info: " + throwable.getMessage());
}
}
}

View File

@@ -16,9 +16,9 @@ public class ClipboardManager {
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
private int getMethodVersion;
private int setMethodVersion;
private int addListenerMethodVersion;
private boolean alternativeGetMethod;
private boolean alternativeSetMethod;
private boolean alternativeAddListenerMethod;
public ClipboardManager(IInterface manager) {
this.manager = manager;
@@ -31,20 +31,9 @@ public class ClipboardManager {
} else {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
} catch (NoSuchMethodException e3) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
}
}
} catch (NoSuchMethodException e) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
alternativeGetMethod = true;
}
}
}
@@ -58,64 +47,41 @@ public class ClipboardManager {
} else {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
} catch (NoSuchMethodException e2) {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
}
} catch (NoSuchMethodException e) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
alternativeSetMethod = true;
}
}
}
return setPrimaryClipMethod;
}
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
switch (methodVersion) {
case 0:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
case 1:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
if (alternativeMethod) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
}
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
} else if (alternativeMethod) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
}
public CharSequence getText() {
try {
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
@@ -130,7 +96,7 @@ public class ClipboardManager {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, setMethodVersion, manager, clipData);
setPrimaryClip(method, alternativeSetMethod, manager, clipData);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
@@ -138,23 +104,14 @@ public class ClipboardManager {
}
}
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return;
}
switch (methodVersion) {
case 0:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
break;
case 1:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
break;
default:
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
break;
} else if (alternativeMethod) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
} else {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
}
}
@@ -167,19 +124,10 @@ public class ClipboardManager {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
addListenerMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class);
addListenerMethodVersion = 1;
} catch (NoSuchMethodException e2) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
int.class, int.class);
addListenerMethodVersion = 2;
}
} catch (NoSuchMethodException e) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
alternativeAddListenerMethod = true;
}
}
}
@@ -189,7 +137,7 @@ public class ClipboardManager {
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);