Compare commits

...

138 Commits

Author SHA1 Message Date
233f8e6cc4 Rename keycode injection method
Make it explicit that it injects both "press" and "release" events.
2021-04-30 23:07:37 +02:00
9a7d351d67 Simplify non-static injectEvent() implementation
Just call the static version (having a displayId) from the non-static
version (using the displayId field).
2021-04-30 23:07:37 +02:00
d00ee640c0 Simplify Device.java
Remove useless intermediate method with a "mode" parameter (it's always
called with the same mode).

This also avoids the need for a specific injectEventOnDisplay() method,
since it does not conflict with another injectEvent() method anymore.
2021-04-30 23:07:29 +02:00
ae6ec7a194 Unref decoder AVFrame immediately
The frame can be unref immediately after it is pushed to the frame
sinks.

It was not really a memory leak because the frame was unref every time
by avcodec_receive_frame() (and freed on close), but a reference was
unnecessarily kept for too long.
2021-04-26 18:05:43 +02:00
84f17fdeab Fix v4l2 AVPacket memory leak on error
Unref v4l2 AVPacket even if writing failed.
2021-04-26 18:05:11 +02:00
1cde68a1fa Fix v4l2 AVFrame memory leak
Unref frame immediately once encoded.

Fixes #2279 <https://github.com/Genymobile/scrcpy/pull/2279>
2021-04-26 18:04:51 +02:00
45e7280148 Rename --v4l2_sink to --v4l2-sink
This was a rebase issue, the previous version in #2268 was correct.

Refs #2268 <https://github.com/Genymobile/scrcpy/pull/2268>
2021-04-26 17:59:35 +02:00
41a0383d7c Document v4l2 sink in README 2021-04-25 15:00:56 +02:00
d39161f753 Add support for v4l2loopback
It allows to send the video stream to /dev/videoN, so that it can be
captured (like a webcam) by any v4l2-capable tool.

PR #2232 <https://github.com/Genymobile/scrcpy/pull/2232>
PR #2233 <https://github.com/Genymobile/scrcpy/pull/2233>
PR #2268 <https://github.com/Genymobile/scrcpy/pull/2268>

Co-authored-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:59:10 +02:00
5af9d0ee0f Make --lock-video-orientation argument optional
If the option is set without argument, lock the initial device
orientation (as if the value "initial" was passed).
2021-04-25 14:55:54 +02:00
fd0dc6c0cd Add --lock-video-orientation=initial
Add a new mode to the --lock-video-orientation option, to lock the
initial orientation of the device.

This avoids to pass an explicit value (0, 1, 2 or 3) and think about
which is the right one.
2021-04-25 14:55:54 +02:00
151bc16644 Use strlist_contains() to find a muxer
The AVOutputFormat name is a string list: it contains names separated by
',' (possibly only one).
2021-04-25 14:55:19 +02:00
ffc00210e9 Add strlist_contains()
Add a function to know if a string list, using some separator, contains
a specific string.
2021-04-25 14:38:42 +02:00
243854a408 Fix recorder comment 2021-04-25 14:38:42 +02:00
8b90dc61b9 Handle EAGAIN on send_packet in decoder
EAGAIN was only handled on receive_frame.

In practice, it should not be necessary, since one packet always
contains one frame. But just in case.
2021-04-25 14:38:42 +02:00
2a5dfc1c17 Handle errors using gotos in recorder_open()
There are many initializations in recorder_open(). Handle RAII-like
deinitialization using gotos.
2021-04-25 14:38:42 +02:00
e3fccc5a5e Initialize recorder fields on open
Only initialize ops and parameters copy from recorder_init(). It was
inconsistent to initialize some fields from _init() and some others from
_open().
2021-04-25 14:38:42 +02:00
0541f1bff2 Hide the window immediately on close
The screen may not be destroyed immediately on close to avoid undefined
behavior, because it may still receive events from the decoder.

But the visual window must still be closed immediately.
2021-04-25 14:38:42 +02:00
0272e6dc77 Assert screen closed on destroy
The destruction order is important, but tricky, because the screen is
open/close by the decoder, but destroyed by scrcpy.c on the main thread.

Add assertions to guarantee that the screen is not destroyed before
being closed.
2021-04-25 14:38:42 +02:00
2a94a2b119 Remove video_buffer callbacks
Now that screen is both the owner and the listener of the video buffer,
execute the code directly without callbacks.
2021-04-25 14:38:42 +02:00
e91acdb0c4 Move video_buffer to screen
The video buffer is now an internal detail of the screen component.

Since the screen is plugged to the decoder via the frame sink trait, the
decoder does not access to the video buffer anymore.
2021-04-25 14:38:42 +02:00
6f5ad21f57 Make decoder push frames to sinks
Now that screen implements the packet sink trait, make decoder push
packets to the sinks without depending on the concrete sink types.
2021-04-25 14:38:42 +02:00
08b3086ffc Expose screen as frame sink
Make screen implement the frame sink trait.

This will allow the decoder to push frames without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
deab7da761 Add frame sink trait
This trait will allow to abstract the concrete sink types from the frame
producer (decoder.c).
2021-04-25 14:38:42 +02:00
f7a1b67d66 Make stream push packets to sinks
Now that decoder and recorder implement the packet sink trait, make
stream push packets to the sinks without depending on the concrete sink
types.
2021-04-25 14:38:42 +02:00
cbed38799e Expose decoder as packet sink
Make decoder implement the packet sink trait.

This will allow the stream to push packets without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
5beb7d6c02 Reorder decoder functions
This will make further commits more readable.
2021-04-25 14:38:42 +02:00
5980183a33 Expose recorder as packet sink
Make recorder implement the packet sink trait.

This will allow the stream to push packets without depending on the
concrete sink type.
2021-04-25 14:38:42 +02:00
fe8de893ca Privatize recorder threading
The fact that the recorder uses a separate thread is an internal detail,
so the functions _start(), _stop() and _join() should not be exposed.

Instead, start the thread on _open() and _stop()+_join() on close().

This paves the way to expose the recorder as a packet sink trait.
2021-04-25 14:38:42 +02:00
a974483c15 Reorder recorder functions
This will make further commits more readable.
2021-04-25 14:38:42 +02:00
1b072a24c4 Add packet sink trait
This trait will allow to abstract the concrete sink types from the
packet producer (stream.c).
2021-04-25 14:38:42 +02:00
08f1fd46c8 Add container_of() macro
This will allow to get the parent of an embedded struct.
2021-04-25 14:38:42 +02:00
2ddf760c09 Make video_buffer more generic
The video buffer took ownership of the producer frame (so that it could
swap frames quickly).

In order to support multiple sinks plugged to the decoder, the decoded
frame must not be consumed by the display video buffer.

Therefore, move the producer and consumer frames out of the video
buffer, and use FFmpeg AVFrame refcounting to share ownership while
avoiding copies.
2021-04-25 14:38:42 +02:00
5d9e96dc4e Remove compat with old FFmpeg codec params API
The new API has been introduced in 2016 in libavformat 57.xx, it's very
old.

This will avoid to maintain two code paths for codec parameters.
2021-04-25 14:38:42 +02:00
de9b79ec2d Remove compat with old FFmpeg decoding API
The new API has been introduced in 2016 in libavcodec 57.xx, it's very
old.

This will avoid to maintain two code paths for decoding.
2021-04-25 14:38:42 +02:00
55806e7d31 Remove option --render-expired-frames
This flag forced the decoder to wait for the previous frame to be
consumed by the display.

It was initially implemented as a compilation flag for testing, not
intended to be exposed at runtime. But to remove ifdefs and to allow
users to test this flag easily, it had finally been exposed by commit
ebccb9f6cc.

In practice, it turned out to be useless: it had no practical impact,
and it did not solve or mitigate any performance issues causing frame
skipping.

But that added some complexity to the codebase: it required an
additional condition variable, and made video buffer calls possibly
blocking, which in turn required code to interrupt it on exit.

To prepare support for multiple sinks plugged to the decoder (display
and v4l2 for example), the blocking call used for pacing the decoder
output becomes unacceptable, so just remove this useless "feature".
2021-04-25 14:38:42 +02:00
21b590b766 Write trailer from recorder thread
The recorder thread wrote the whole content except the trailer, which
was odd.
2021-04-25 14:38:42 +02:00
d7e6589677 Document 4th+5th + 2xn shortcuts
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
b4ee9f27ce Add mouse shortcut to expand settings panel
Double-click on extra mouse button to open the settings panel (a
single-click opens the notification panel).

This is consistent with the keyboard shortcut MOD+n+n.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
6fa63cf6f8 Add keyboard shortcut to expand settings panel
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
50eecdab28 Add control message to expand settings panel
PR #2260 <https://github.com/Genymobile/scrcpy/pull/2260>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
9576283907 Count repeated identical key events
This will allow shortcuts such as MOD+n+n to open the settings panel.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
66c581851f Rename control message type to COLLAPSE_PANELS
The collapsing action collapses any panels.

By the way, the Android method is named collapsePanels().

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-25 14:36:48 +02:00
bb4614d558 Reverse boolean logic for readability
Refs #2260 <https://github.com/Genymobile/scrcpy/pull/2260#issuecomment-823508759>
2021-04-25 14:36:48 +02:00
aaf7875d92 Ensure get_server_path() retval is freeable
PR #2276 <https://github.com/Genymobile/scrcpy/pull/2276>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-22 22:12:23 +02:00
b9c3f65fd8 Provide actions for the extra mouse buttons
Bind APP_SWITCH to button 4 and expand notification panel on button 5.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-04-19 20:16:45 +02:00
d0739911a3 Forward DOWN and UP separately for right-click
The shortcut "back on screen on" is a bit special: the control is
requested by the client, but the actual event injection (POWER or BACK)
is determined on the device.

To properly inject DOWN and UP events for BACK, transmit the action as
a control parameter.

If the screen is off:
 - on DOWN, inject POWER (DOWN and UP) (wake up the device immediately)
 - on UP, do nothing
If the screen is on:
 - on DOWN, inject BACK DOWN
 - on UP, inject BACK UP

A corner case is when the screen turns off between the DOWN and UP
event. In that case, a BACK UP event will be injected, so it's harmless.

As a consequence of this change, the BACK button is now handled by
Android on mouse released. This is consistent with the keyboard shortcut
(Mod+b) behavior.

PR #2259 <https://github.com/Genymobile/scrcpy/pull/2259>
Refs #2258 <https://github.com/Genymobile/scrcpy/pull/2258>
2021-04-19 20:16:45 +02:00
964b6d2243 Forward DOWN and UP separately for middle-click
As a consequence of this change, the HOME button is now handled by
Android on mouse released. This is consistent with the keyboard shortcut
(MOD+h) behavior.

PR #2259 <https://github.com/Genymobile/scrcpy/pull/2259>
Refs #2258 <https://github.com/Genymobile/scrcpy/pull/2258>
2021-04-17 18:04:24 +02:00
8cc057c8f1 Prevent forwarding only "mouse released" events
Some mouse clicks DOWN are captured for shortcuts, but the matching UP
events were still forwarded to the device.

Instead, capture both DOWN and UP for shortcuts, and do nothing on UP.

PR #2259 <https://github.com/Genymobile/scrcpy/pull/2259>
Refs #2258 <https://github.com/Genymobile/scrcpy/pull/2258>
2021-04-17 18:04:13 +02:00
edee69d637 Fix options alphabetical order
"verbosity" < "version"
2021-04-16 17:40:39 +02:00
8ef4c044fa Do not forward SDL_DROPFILE event
The event is handled by scrcpy.c, it is not necessary to send it to
screen or input_manager.
2021-04-13 22:37:08 +02:00
c23c38f99d Move resizing workaround to screen.c 2021-04-13 22:36:59 +02:00
65c4f487b3 Set initial fullscreen from screen.c 2021-04-13 22:15:05 +02:00
c6d7f5ee96 Make screen_show_window() static
It is only used from screen.c now.
2021-04-13 22:04:38 +02:00
28f6cbaea6 Destroy screen once stream is finished
The screen receives callbacks from the decoder, fed by the stream.

The decoder is run from the stream thread, so waiting for the end of
stream is sufficient to avoid possible use-after-destroy.
2021-04-11 14:42:09 +02:00
08fc6694e1 Do not destroy uninitialized screen
When --no-display was passed, screen_destroy() was called while
screen_init() was never called.

In practice, it did not crash because it just freed NULL pointers, but
it was still incorrect.
2021-04-11 13:07:44 +02:00
d0983db592 Make internal recorder function static 2021-04-10 18:48:52 +02:00
fb7870500a Remove unused field from input_manager 2021-04-10 18:48:52 +02:00
33006561c7 Remove useless forward declaration from stream.h 2021-04-10 18:48:52 +02:00
a09733d175 Remove useless includes from decoder.c 2021-04-10 18:48:44 +02:00
6231f683af Fix compilation error for old decoding API
Commits cb9c42bdcb and
441d3fb119 updated the code only for the
new decoding API.
2021-04-05 22:35:14 +02:00
9826c5c4a4 Remove HiDPI compilation flag
Always enable HiDPI support, there is no reason to expose a compilation
flag.
2021-04-04 15:00:13 +02:00
1d615a0d51 Support power off on close
PR #824 <https://github.com/Genymobile/scrcpy/pull/824>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-03-16 21:12:35 +01:00
fb0bcaebc2 Export static method to power off screen in Device
PR #824 <https://github.com/Genymobile/scrcpy/pull/824>

Signed-off-by: Yu-Chen Lin <npes87184@gmail.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-03-16 21:12:29 +01:00
dd453ad041 Pass scrcpy-noconsole arguments through to scrcpy
PR #2052 <https://github.com/Genymobile/scrcpy/pull/2052>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-03-16 21:04:04 +01:00
0308ef43f2 Do not set buttons on touch events
BUTTON_PRIMARY must not be set for touch events:

> This button constant is not set in response to simple touches with a
> finger or stylus tip. The user must actually push a button.

<https://developer.android.com/reference/android/view/MotionEvent#BUTTON_PRIMARY>

Fixes #2169 <https://github.com/Genymobile/scrcpy/issues/2169>
2021-03-15 18:46:56 +01:00
40febf4a91 Use device id 0 for touch/mouse events
Virtual device is only for keyboard sources, not mouse or touchscreen
sources. Here is the value of InputDevice.getDevice(-1).toString():

    Input Device -1: Virtual
      Descriptor: ...
      Generation: 2
      Location: built-in
      Keyboard Type: alphabetic
      Has Vibrator: false
      Has mic: false
      Sources: 0x301 ( keyboard dpad )

InputDevice.getDeviceId() documentation says:

> An id of zero indicates that the event didn't come from a physical
> device and maps to the default keymap.

<https://developer.android.com/reference/android/view/InputEvent#getDeviceId()>

However, injecting events with a device id of 0 causes event.getDevice()
to be null on the client-side.

Commit 26529d377f used -1 as a workaround
to avoid a NPE on a specific Android TV device. But this is a bug in the
device system, which wrongly assumes that input device may not be null.

A similar issue was present in Flutter, but it is now fixed:
 - <https://github.com/flutter/flutter/issues/30665>
 - <https://github.com/flutter/engine/pull/7986>

On the other hand, using an id of -1 for touchscreen events (which is
invalid) causes issues for some apps:
<https://github.com/Genymobile/scrcpy/issues/2125#issuecomment-790535792>

Therefore, use a device id of 0.

An alternative could be to find an existing device matching the source,
like "adb shell input" does. See getInputDeviceId():
<https://android.googlesource.com/platform/frameworks/base.git/+/master/cmds/input/src/com/android/commands/input/Input.java>

But it seems better to indicate that the event didn't come from a
physical device, and it would not solve #962 anyway, because an Android
TV has no touchscreen.

Refs #962 <https://github.com/Genymobile/scrcpy/issues/962>
Fixes #2125 <https://github.com/Genymobile/scrcpy/issues/2125>
2021-03-14 18:30:56 +01:00
429fdef04f Fix encoder parameter suggestion
The option is --encoder, not --encoder-name.
2021-03-11 10:55:12 +01:00
d1789f082a meson: Do not use full path with mingw tools name
This helps to use mingw toolchains which are not in /usr/bin path.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-03-09 09:59:57 +01:00
eb7e1070cf Release frame data as soon as possible
During a frame swap, one of the two frames involved can be released.
2021-03-06 22:58:03 +01:00
386f017ba9 Factorize frame swap 2021-03-06 22:58:03 +01:00
cc48b24324 Simplify screen initialization
Use a single function to initialize the screen instance.
2021-03-06 22:58:03 +01:00
597c54f049 Group screen parameters into a struct
The function screen_init_rendering had too many parameters.
2021-03-06 22:58:03 +01:00
955da3b578 Remove screen static initializer
Most of the fields are initialized dynamically.
2021-03-06 22:58:03 +01:00
cb9c42bdcb Use a callback to notify frame skip
A skipped frame is detected when the producer offers a frame while the
current pending frame has not been consumed.

However, the producer (in practice the decoder) is not interested in the
fact that a frame has been skipped, only the consumer (the renderer) is.

Therefore, notify frame skip via a consumer callback. This allows to
manage the skipped and rendered frames count at the same place, and
remove fps_counter from decoder.
2021-03-06 22:58:03 +01:00
fb9f9848bd Use a callback to notify a new frame
Make the decoder independant of the SDL even mechanism, by making the
consumer register a callback on the video_buffer.
2021-03-06 22:58:03 +01:00
c50b958ee4 Initialize screen before starting the stream
As soon as the stream is started, the video buffer could notify a new
frame available.

In order to pass this event to the screen without race condition, the
screen must be initialized before the screen is started.
2021-03-06 22:58:03 +01:00
441d3fb119 Make video buffer more generic
Video buffer is a tool between a frame producer and a frame consumer.

For now, it is used between a decoder and a renderer, but in the future
another instance might be used to swscale decoded frames.
2021-03-06 22:58:03 +01:00
cb197ee3a2 Move fps counter out of video buffer
In order to make video buffer more generic, move out its specific
responsibility to count the fps between the decoder and the renderer.
2021-03-06 22:58:03 +01:00
218636dc10 Inject touch events with smallest detectable size
A value of 1 means the "largest detectable size", a value of 0 means the
"smallest detectable size".

https://developer.android.com/reference/android/view/MotionEvent.PointerCoords#size
https://developer.android.com/reference/android/view/MotionEvent#AXIS_SIZE

Fixes #2125 <https://github.com/Genymobile/scrcpy/issues/2125>
2021-03-03 18:22:54 +01:00
b16b65a715 Simplify default values
It makes sense to extract default values for bitrate and port range
(which are arbitrary and might be changed in the future).

However, the default values for "max size" and "lock video orientation"
are naturally unlimited/unlocked, and will never be changed. Extracting
these options just added complexity for no benefit, so hardcode them.
2021-02-25 22:19:05 +01:00
a3aa5ac95e Insert numerical values statically in usage string 2021-02-25 22:19:05 +01:00
0207e3df33 Remove unused no_window field 2021-02-25 22:19:05 +01:00
9cd1a7380d Enable NDEBUG via Meson built-in option 2021-02-25 22:19:05 +01:00
24b637b972 Handle im-related events from input_manager.c 2021-02-25 22:19:05 +01:00
76a3d9805b Inline window events handling
Now that all screen-related events are handled from screen.c, there is
no need for a separate method for window events.
2021-02-25 22:19:05 +01:00
50b4a730e3 Handle screen-related events from screen.c 2021-02-25 22:19:05 +01:00
ea2369f568 Reference video buffer from screen
This paves the way to handle EVENT_NEW_FRAME from screen.c, by allowing
to call screen_update_frame() without an explicit video_buffer instance.
2021-02-25 22:19:05 +01:00
0538e9645b Improve error handling in screen initialization
After the struct screen is initialized, the window, the renderer and the
texture are necessarily valid, so there is no need to check in
screen_destroy().
2021-02-25 22:18:51 +01:00
626094ad13 Handle window events only once visible
This will avoid corner cases where we need to resize while no frame has
been received yet.
2021-02-17 09:54:03 +01:00
a566635c43 Log mipmaps error only if mipmaps are enabled 2021-02-17 09:54:03 +01:00
862948b132 Make use_opengl local
The flag is used only locally, there is no need to store it in the
screen structure.
2021-02-17 09:54:03 +01:00
c0c4ba7009 Add intermediate frame in video buffer
There were only two frames simultaneously:
 - one used by the decoder;
 - one used by the renderer.

When the decoder finished decoding a frame, it swapped it with the
rendering frame.

Adding a third frame provides several benefits:
 - the decoder do not have to wait for the renderer to release the
   mutex;
 - it simplifies the video_buffer API;
 - it makes the rendering frame valid until the next call to
   video_buffer_take_rendering_frame(), which will be useful for
   swscaling on window resize.
2021-02-17 09:54:03 +01:00
c53bd4d8b6 Assert non-recursive usage of mutexes 2021-02-17 09:54:03 +01:00
54f5c42d7b Add mutex assertions 2021-02-17 09:54:03 +01:00
21d206f360 Expose mutex assertions
Add a function to assert that the mutex is held (or not).
2021-02-17 09:54:03 +01:00
d2689fc168 Expose thread id 2021-02-17 09:54:03 +01:00
f6320c7e31 Wrap SDL thread functions into scrcpy-specific API
The goal is to expose a consistent API for system tools, and paves the
way to make the "core" independant of SDL in the future.
2021-02-17 09:54:03 +01:00
30e619d37f Replace SDL_strdup() by strdup()
The functions SDL_malloc(), SDL_free() and SDL_strdup() were used only
because strdup() was not available everywhere.

Now that it is available, use the native version of these functions.
2021-02-17 09:54:03 +01:00
c0dde0fade Provide strdup() compat
Make strdup() available on all platforms.
2021-02-17 09:53:25 +01:00
ace438e52a Remove unused port_range field
The port_range is used from "struct server_params", the copy in
"struct server" was unused.
2021-02-14 14:47:49 +01:00
8e83f3e8af Remove unused custom event 2021-02-14 14:44:05 +01:00
97b001e7c0 Fix undefined left shift
Small unsigned integers promote to signed int. As a consequence, if v is
a uint8_t, then (v << 24) yields an int, so the left shift is undefined
if the MSB is 1.

Cast to uint32_t to yield an unsigned value.

Reported by USAN (meson x -Db_sanitize=undefined):

    runtime error: left shift of 255 by 24 places cannot be represented
    in type 'int'
2021-01-24 15:31:21 +01:00
d8e9ad20b0 Improve file handler error message
Terminating the file handler current process may be either a "push" or
"install" command.
2021-01-24 14:24:24 +01:00
b566700bfd Kill process with SIGKILL signal
An "adb push" command is not terminated by SIGTERM.
2021-01-24 14:24:24 +01:00
7afd149f4b Fix file_handler process race condition
The current process could be waited both by run_file_handler() and
file_handler_stop().

To avoid the race condition, wait the process without closing, then
close with mutex locked.
2021-01-24 14:24:24 +01:00
6a50231698 Expose a single process_wait()
There were two versions: process_wait() and process_wait_noclose().

Expose a single version with a flag (it was already implemented that way
internally).
2021-01-24 14:24:18 +01:00
b8edcf52b0 Simplify process_wait()
The function process_wait() returned a bool (true if the process
terminated successfully) and provided the exit code via an output
parameter exit_code.

But the returned value was always equivalent to exit_code == 0, so just
return the exit code instead.
2021-01-22 18:29:21 +01:00
94eff0a4bb Fix size_t incorrectly assigned to int
The function control_msg_serialize() returns a size_t.
2021-01-17 19:44:23 +01:00
8dbb1676b7 Factorize meson compiler variable initialization 2021-01-17 19:44:23 +01:00
ab912c23e7 Define feature test macros in common.h
This enables necessary functions once for all.

As a consequence, define common.h before any other header.
2021-01-17 14:08:48 +01:00
59feb2a15c Group common includes into common.h
Include config.h and compat.h in common.h, and include common.h from all
source files.
2021-01-08 19:22:10 +01:00
6385b8c162 Move common structs to coords.h
The size, point and position structs were defined in common.h. Move them
to coords.h so that common.h could be used for generic code to be
included in all source files.
2021-01-08 19:22:10 +01:00
037be4af21 Fix compat missing include
The header libavformat/version.h was included, but not
libavcodec/version.h.

As a consequence, the LIBAVCODEC_VERSION_INT definition depended on the
caller includes.
2021-01-08 19:21:54 +01:00
1e215199dc Remove unused struct port_range
It had been replaced by struct sc_port_range in scrcpy.h.
2021-01-08 19:13:53 +01:00
d580ee30f1 Separate process wait and close
On Linux, waitpid() both waits for the process to terminate and reaps it
(closes its handle). On Windows, these actions are separated into
WaitForSingleObject() and CloseHandle().

Expose these actions separately, so that it is possible to send a signal
to a process while waiting for its termination without race condition.

This allows to wait for server termination normally, but kill the
process without race condition if it is not terminated after some delay.
2021-01-08 16:44:21 +01:00
821c175730 Rename process_simple_wait to process_wait
Adding "simple" in the function name brings no benefit.
2021-01-08 16:44:21 +01:00
cc6f5020d8 Move conditional src files in meson.build
Declare all the source files (including the platform-specific ones) at
the beginning.
2021-01-08 16:44:21 +01:00
4bd9da4c93 Split command into process and adb
The process API provides the system-specific implementation, the adb API
uses it to expose adb commands.
2021-01-08 16:44:21 +01:00
aa8b571389 Increase display id range
Some devices use big display id values.

Refs #2009 <https://github.com/Genymobile/scrcpy/issues/2009>
2021-01-04 08:16:32 +01:00
ed130e05d5 Fix possibly uninitialized value
Due to gotos, "ret" may be returned uninitialized.
2021-01-03 22:41:51 +01:00
192fbd8450 Use --cask for latest versions of brew
PR #2004 <https://github.com/Genymobile/scrcpy/pull/2004>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2021-01-02 18:27:38 +01:00
c5c5fc18ae Update links to v1.17 in README and BUILD 2021-01-02 01:26:23 +01:00
f682b87ba5 Bump version to 1.17 2021-01-02 00:53:32 +01:00
10b749e27d Kill the server only after a small delay
Let the server terminate properly once all the sockets are closed.

If it does not terminate (this can happen if the device is asleep), then
kill it.

Note: since the server process termination is detected by a flag set
after waitpid() returns, there is a small chance that the process
terminates (and the PID assigned to a new process) before the flag is
set but before the kill() call. This race condition already existed
before this commit.

Fixes #1992 <https://github.com/Genymobile/scrcpy/issues/1992>
2021-01-01 23:57:01 +01:00
05e8c1a3c5 Call CloseHandle() after wait on Windows
TerminateProcess() is "equivalent" to kill(), while
WaitForSingleObject() is "equivalent" to waitpid(), so the handle must
be closed after WaitForSingleObject().
2021-01-01 23:57:01 +01:00
83910d3b9c Initialize server struct dynamically
This will allow to add mutex/cond fields.
2021-01-01 17:40:52 +01:00
90f8356630 Interrupt device threads on stop
The (non-daemon) threads were not interrupted on video stream stopped,
leaving the server process alive.

Interrupt them to wake up their blocking call so that they terminate
properly.

Refs #1992 <https://github.com/Genymobile/scrcpy/issues/1992>
2021-01-01 17:32:34 +01:00
3ba51211d6 Mention how to add default arguments on Windows
Mention that it is possible to add default arguments by editing the
wrapper scripts.
2021-01-01 17:31:07 +01:00
ea3582d2c3 Merge branch 'master' into dev 2021-01-01 17:18:13 +01:00
112adbba87 Happy new year 2021! 2021-01-01 17:16:44 +01:00
d039a7a39a Upgrade SDL (2.0.14) for Windows
Include the latest version of SDL in Windows releases.
2021-01-01 16:08:58 +01:00
6ab80e4ce8 Rename release.make to release.mk
It's more standard, and benefits from syntax coloration in vi.
2021-01-01 15:51:10 +01:00
230afd8966 Unify release makefile
Before this change, release.sh built some native stuff, and
Makefile.CrossWindows built the Windows releases.

Instead, use a single release.make to build the whole release. It also
avoids to build the server one more time.
2020-12-22 18:52:05 +01:00
a46733906a Replace noconsole binary by a wrapper script
This simplifies the build system.

Refs <https://github.com/Genymobile/scrcpy/issues/1975#issuecomment-745314161>
2020-12-22 18:51:59 +01:00
431c9ee33b Improve rotation documentation 2020-12-22 01:48:01 +01:00
43d3dcbd97 Document Windows command line usage
PR #1973 <https://github.com/Genymobile/scrcpy/pull/1973>

Reviewed-by: Yu-Chen Lin <npes87184@gmail.com>
2020-12-22 01:44:55 +01:00
a5f4f58295 Remove duplicate include 2020-12-22 01:14:30 +01:00
102 changed files with 2802 additions and 1559 deletions

View File

@ -254,10 +254,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server ## Prebuilt server
- [`scrcpy-server-v1.16`][direct-scrcpy-server] - [`scrcpy-server-v1.17`][direct-scrcpy-server]
_(SHA-256: 94a79e05b4498d0460ab7bd9d12cbf05156e3a47bf0c5d1420cee1d4493b3832)_ _(SHA-256: 11b5ad2d1bc9b9730fb7254a78efd71a8ff46b1938ff468e47a21b653a1b6725_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-server-v1.16 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-server-v1.17
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

36
FAQ.md
View File

@ -201,3 +201,39 @@ scrcpy -m 800
``` ```
You could also try another [encoder](README.md#encoder). You could also try another [encoder](README.md#encoder).
## Command line on Windows
Some Windows users are not familiar with the command line. Here is how to open a
terminal and run `scrcpy` with arguments:
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/

View File

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

View File

@ -675,7 +675,7 @@ Baca [halaman pengembang].
## Lisensi ## Lisensi
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -477,7 +477,7 @@ _²화면이 꺼진 상태에서 우클릭 시 다시 켜지며, 그 외의 상
## 라이선스 ## 라이선스
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

116
README.md
View File

@ -1,4 +1,4 @@
# scrcpy (v1.16) # scrcpy (v1.17)
[Read in another language](#translations) [Read in another language](#translations)
@ -77,10 +77,10 @@ hard).
For Windows, for simplicity, a prebuilt archive with all the dependencies For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available: (including `adb`) is available:
- [`scrcpy-win64-v1.16.zip`][direct-win64] - [`scrcpy-win64-v1.17.zip`][direct-win64]
_(SHA-256: 3f30dc5db1a2f95c2b40a0f5de91ec1642d9f53799250a8c529bc882bc0918f0)_ _(SHA-256: 8b9e57993c707367ed10ebfe0e1ef563c7a29d9af4a355cd8b6a52a317c73eea)_
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.16/scrcpy-win64-v1.16.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.17/scrcpy-win64-v1.17.zip
It is also available in [Chocolatey]: It is also available in [Chocolatey]:
@ -116,6 +116,10 @@ brew install scrcpy
You need `adb`, accessible from your `PATH`. If you don't have it yet: You need `adb`, accessible from your `PATH`. If you don't have it yet:
```bash ```bash
# Homebrew >= 2.6.0
brew install --cask android-platform-tools
# Homebrew < 2.6.0
brew cask install android-platform-tools brew cask install android-platform-tools
``` ```
@ -194,6 +198,7 @@ If `--max-size` is also specified, resizing is applied after cropping.
To lock the orientation of the mirroring: To lock the orientation of the mirroring:
```bash ```bash
scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation 0 # natural orientation scrcpy --lock-video-orientation 0 # natural orientation
scrcpy --lock-video-orientation 1 # 90° counterclockwise scrcpy --lock-video-orientation 1 # 90° counterclockwise
scrcpy --lock-video-orientation 2 # 180° scrcpy --lock-video-orientation 2 # 180°
@ -202,6 +207,8 @@ scrcpy --lock-video-orientation 3 # 90° clockwise
This affects recording orientation. This affects recording orientation.
The [window may also be rotated](#rotation) independently.
#### Encoder #### Encoder
@ -219,7 +226,9 @@ error will give the available encoders:
scrcpy --encoder _ scrcpy --encoder _
``` ```
### Recording ### Capture
#### Recording
It is possible to record the screen while mirroring: It is possible to record the screen while mirroring:
@ -243,6 +252,58 @@ variation] does not impact the recorded file.
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation [packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
#### v4l2loopback
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.
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 -N # --no-display to disable mirroring window
```
(replace `N` by 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].
[OBS]: https://obsproject.com/fr
### Connection ### Connection
#### Wireless #### Wireless
@ -407,9 +468,9 @@ Note that _scrcpy_ manages 3 different rotations:
- <kbd>MOD</kbd>+<kbd>r</kbd> requests the device to switch between portrait - <kbd>MOD</kbd>+<kbd>r</kbd> requests the device to switch between portrait
and landscape (the current running app may refuse, if it does support the and landscape (the current running app may refuse, if it does support the
requested orientation). requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation - [`--lock-video-orientation`](#lock-video-orientation) changes the mirroring
of the video sent from the device to the computer). This affects the orientation (the orientation of the video sent from the device to the
recording. computer). This affects the recording.
- `--rotation` (or <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>) - `--rotation` (or <kbd>MOD</kbd>+<kbd>←</kbd>/<kbd>MOD</kbd>+<kbd>→</kbd>)
rotates only the window content. This affects only the display, not the rotates only the window content. This affects only the display, not the
recording. recording.
@ -485,18 +546,6 @@ scrcpy -Sw
``` ```
#### Render expired frames
By default, to minimize latency, _scrcpy_ always renders the last decoded frame
available, and drops any previous one.
To force the rendering of all frames (at a cost of a possible increased
latency), use:
```bash
scrcpy --render-expired-frames
```
#### Show touches #### Show touches
For presentations, it may be useful to show physical touches (on the physical For presentations, it may be useful to show physical touches (on the physical
@ -692,10 +741,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_ | Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_ | 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 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_ | Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_ | Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> | 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 `MENU` (unlock screen) | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_ | Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_ | Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
@ -704,18 +753,27 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd> | 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> | Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd> | Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> | Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd> | Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd> | Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd> | Copy to clipboard | <kbd>MOD</kbd>+<kbd>c</kbd>
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</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> | 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> | Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_ | Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
_¹Double-click on black borders to remove them._ _¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._ _²Right-click turns the screen on if it was off, presses BACK otherwise._
Only on Android >= 7._ 4th and 5th mouse buttons, if your mouse has them._
_⁴Only on Android >= 7._
Shortcuts with repeated keys are executted 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 All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application. handled by the active application.
@ -766,7 +824,7 @@ Read the [developers page].
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -508,7 +508,7 @@ Leia a [developers page].
## Licença ## Licença
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -703,7 +703,7 @@ _³需要安卓版本 Android >= 7。_
## 许可协议 ## 许可协议
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -682,7 +682,7 @@ _³只支援 Android 7+。_
## Licence ## Licence
Copyright (C) 2018 Genymobile Copyright (C) 2018 Genymobile
Copyright (C) 2018-2020 Romain Vimont Copyright (C) 2018-2021 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@ -1,7 +1,8 @@
src = [ src = [
'src/main.c', 'src/main.c',
'src/adb.c',
'src/cli.c', 'src/cli.c',
'src/command.c', 'src/compat.c',
'src/control_msg.c', 'src/control_msg.c',
'src/controller.c', 'src/controller.c',
'src/decoder.c', 'src/decoder.c',
@ -21,9 +22,28 @@ src = [
'src/tiny_xpm.c', 'src/tiny_xpm.c',
'src/video_buffer.c', 'src/video_buffer.c',
'src/util/net.c', 'src/util/net.c',
'src/util/str_util.c' 'src/util/process.c',
'src/util/str_util.c',
'src/util/thread.c',
] ]
if host_machine.system() == 'windows'
src += [ 'src/sys/win/process.c' ]
else
src += [ 'src/sys/unix/process.c' ]
endif
v4l2_support = host_machine.system() == 'linux'
if v4l2_support
src += [ 'src/v4l2_sink.c' ]
endif
check_functions = [
'strdup'
]
cc = meson.get_compiler('c')
if not get_option('crossbuild_windows') if not get_option('crossbuild_windows')
# native build # native build
@ -34,11 +54,13 @@ if not get_option('crossbuild_windows')
dependency('sdl2'), dependency('sdl2'),
] ]
if v4l2_support
dependencies += dependency('libavdevice')
endif
else else
# cross-compile mingw32 build (from Linux to Windows) # cross-compile mingw32 build (from Linux to Windows)
cc = meson.get_compiler('c')
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin' sdl2_bin_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/bin'
sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib' sdl2_lib_dir = meson.current_source_dir() + '/../prebuilt-deps/' + prebuilt_sdl2 + '/lib'
@ -73,19 +95,18 @@ else
endif endif
cc = meson.get_compiler('c')
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
src += [ 'src/sys/win/command.c' ]
dependencies += cc.find_library('ws2_32') dependencies += cc.find_library('ws2_32')
else
src += [ 'src/sys/unix/command.c' ]
endif endif
conf = configuration_data() conf = configuration_data()
# expose the build type foreach f : check_functions
conf.set('NDEBUG', get_option('buildtype') != 'debug') if cc.has_function(f)
define = 'HAVE_' + f.underscorify().to_upper()
conf.set(define, true)
endif
endforeach
# the version, updated on release # the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version()) conf.set_quoted('SCRCPY_VERSION', meson.project_version())
@ -102,48 +123,28 @@ conf.set('PORTABLE', get_option('portable'))
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183') conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default max video size for both dimensions, in pixels
# overridden by option --max-size
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
# the default video orientation
# natural device orientation is 0 and each increment adds 90 degrees
# counterclockwise
# overridden by option --lock-video-orientation
conf.set('DEFAULT_LOCK_VIDEO_ORIENTATION', '-1') # -1: unlocked
# the default video bitrate, in bits/second # the default video bitrate, in bits/second
# overridden by option --bit-rate # overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# disable console on Windows
conf.set('WINDOWS_NOCONSOLE', get_option('windows_noconsole'))
# run a server debugger and wait for a client to be attached # run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger')) conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) # select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
# enable V4L2 support (linux only)
conf.set('HAVE_V4L2', v4l2_support)
configure_file(configuration: conf, output: 'config.h') configure_file(configuration: conf, output: 'config.h')
src_dir = include_directories('src') src_dir = include_directories('src')
if get_option('windows_noconsole')
link_args = [ '-Wl,--subsystem,windows' ]
else
link_args = []
endif
executable('scrcpy', src, executable('scrcpy', src,
dependencies: dependencies, dependencies: dependencies,
include_directories: src_dir, include_directories: src_dir,
install: true, install: true,
c_args: [], c_args: [])
link_args: link_args)
install_man('scrcpy.1') install_man('scrcpy.1')

View File

@ -83,10 +83,12 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP .TP
.BI "\-\-lock\-video\-orientation " value .BI "\-\-lock\-video\-orientation " [value]
Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise. Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
Default is -1 (unlocked). Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
.TP .TP
.BI "\-\-max\-fps " value .BI "\-\-max\-fps " value
@ -155,10 +157,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER .UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
.UE .UE
.TP
.B \-\-render\-expired\-frames
By default, to minimize latency, scrcpy always renders the last available decoded frame, and drops any previous ones. This flag forces to render all frames, at a cost of a possible increased latency.
.TP .TP
.BI "\-\-rotation " value .BI "\-\-rotation " value
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
@ -188,8 +186,10 @@ Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy). It only shows physical touches (not clicks from scrcpy).
.TP .TP
.B \-v, \-\-version .BI "\-\-v4l2-sink " /dev/videoN
Print the version of scrcpy. Output to v4l2loopback device.
It requires to lock the video orientation (see --lock-video-orientation).
.TP .TP
.BI "\-V, \-\-verbosity " value .BI "\-V, \-\-verbosity " value
@ -197,6 +197,10 @@ Set the log level ("debug", "info", "warn" or "error").
Default is "info" for release builds, "debug" for debug builds. Default is "info" for release builds, "debug" for debug builds.
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP .TP
.B \-w, \-\-stay-awake .B \-w, \-\-stay-awake
Keep the device on while scrcpy is running, when the device is plugged in. Keep the device on while scrcpy is running, when the device is plugged in.

View File

@ -1,12 +1,10 @@
#include "command.h" #include "adb.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "config.h"
#include "common.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
@ -70,7 +68,7 @@ show_adb_installation_msg() {
{"pacman", "pacman -S android-tools"}, {"pacman", "pacman -S android-tools"},
}; };
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) { for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
if (cmd_search(pkg_managers[i].binary)) { if (search_executable(pkg_managers[i].binary)) {
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command); LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
return; return;
} }
@ -118,7 +116,7 @@ adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
cmd[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = cmd_execute(cmd, &process); enum process_result r = process_execute(cmd, &process);
if (r != PROCESS_SUCCESS) { if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, cmd); show_adb_err_msg(r, cmd);
return PROCESS_NONE; return PROCESS_NONE;
@ -175,7 +173,7 @@ adb_push(const char *serial, const char *local, const char *remote) {
} }
remote = strquote(remote); remote = strquote(remote);
if (!remote) { if (!remote) {
SDL_free((void *) local); free((void *) local);
return PROCESS_NONE; return PROCESS_NONE;
} }
#endif #endif
@ -184,8 +182,8 @@ adb_push(const char *serial, const char *local, const char *remote) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__ #ifdef __WINDOWS__
SDL_free((void *) remote); free((void *) remote);
SDL_free((void *) local); free((void *) local);
#endif #endif
return proc; return proc;
@ -206,26 +204,8 @@ adb_install(const char *serial, const char *local) {
process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); process_t proc = adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
#ifdef __WINDOWS__ #ifdef __WINDOWS__
SDL_free((void *) local); free((void *) local);
#endif #endif
return proc; return proc;
} }
bool
process_check_success(process_t proc, const char *name) {
if (proc == PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return false;
}
exit_code_t exit_code;
if (!cmd_simple_wait(proc, &exit_code)) {
if (exit_code != NO_EXIT_CODE) {
LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code);
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
return false;
}
return true;
}

34
app/src/adb.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef SC_ADB_H
#define SC_ADB_H
#include "common.h"
#include <stdbool.h>
#include <inttypes.h>
#include "util/process.h"
process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len);
process_t
adb_forward(const char *serial, uint16_t local_port,
const char *device_socket_name);
process_t
adb_forward_remove(const char *serial, uint16_t local_port);
process_t
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t
adb_push(const char *serial, const char *local, const char *remote);
process_t
adb_install(const char *serial, const char *local);
#endif

View File

@ -6,11 +6,13 @@
#include <stdio.h> #include <stdio.h>
#include <unistd.h> #include <unistd.h>
#include "config.h"
#include "scrcpy.h" #include "scrcpy.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
#define STR_IMPL_(x) #x
#define STR(x) STR_IMPL_(x)
void void
scrcpy_print_usage(const char *arg0) { scrcpy_print_usage(const char *arg0) {
fprintf(stderr, fprintf(stderr,
@ -24,7 +26,7 @@ scrcpy_print_usage(const char *arg0) {
" -b, --bit-rate value\n" " -b, --bit-rate value\n"
" Encode the video at the given bit-rate, expressed in bits/s.\n" " Encode the video at the given bit-rate, expressed in bits/s.\n"
" Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n" " Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
" Default is %d.\n" " Default is " STR(DEFAULT_BIT_RATE) ".\n"
"\n" "\n"
" --codec-options key[:type]=value[,...]\n" " --codec-options key[:type]=value[,...]\n"
" Set a list of comma-separated key:type=value options for the\n" " Set a list of comma-separated key:type=value options for the\n"
@ -77,12 +79,15 @@ scrcpy_print_usage(const char *arg0) {
" This is a workaround for some devices not behaving as\n" " This is a workaround for some devices not behaving as\n"
" expected when setting the device clipboard programmatically.\n" " expected when setting the device clipboard programmatically.\n"
"\n" "\n"
" --lock-video-orientation value\n" " --lock-video-orientation [value]\n"
" Lock video orientation to value.\n" " Lock video orientation to value.\n"
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n" " Possible values are \"unlocked\", \"initial\" (locked to the\n"
" initial orientation), 0, 1, 2 and 3.\n"
" Natural device orientation is 0, and each increment adds a\n" " Natural device orientation is 0, and each increment adds a\n"
" 90 degrees rotation counterclockwise.\n" " 90 degrees rotation counterclockwise.\n"
" Default is %d%s.\n" " Default is \"unlocked\".\n"
" Passing the option without argument is equivalent to passing\n"
" \"initial\".\n"
"\n" "\n"
" --max-fps value\n" " --max-fps value\n"
" Limit the frame rate of screen capture (officially supported\n" " Limit the frame rate of screen capture (officially supported\n"
@ -92,7 +97,7 @@ scrcpy_print_usage(const char *arg0) {
" Limit both the width and height of the video to value. The\n" " Limit both the width and height of the video to value. The\n"
" other dimension is computed so that the device aspect-ratio\n" " other dimension is computed so that the device aspect-ratio\n"
" is preserved.\n" " is preserved.\n"
" Default is %d%s.\n" " Default is 0 (unlimited).\n"
"\n" "\n"
" -n, --no-control\n" " -n, --no-control\n"
" Disable device control (mirror the device in read-only).\n" " Disable device control (mirror the device in read-only).\n"
@ -111,7 +116,8 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
" -p, --port port[:port]\n" " -p, --port port[:port]\n"
" Set the TCP port (range) used by the client to listen.\n" " Set the TCP port (range) used by the client to listen.\n"
" Default is %d:%d.\n" " Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":"
STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".\n"
"\n" "\n"
" --prefer-text\n" " --prefer-text\n"
" Inject alpha characters and space as text events instead of\n" " Inject alpha characters and space as text events instead of\n"
@ -140,12 +146,6 @@ scrcpy_print_usage(const char *arg0) {
" \"opengles2\", \"opengles\", \"metal\" and \"software\".\n" " \"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
" <https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>\n" " <https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>\n"
"\n" "\n"
" --render-expired-frames\n"
" By default, to minimize latency, scrcpy always renders the\n"
" last available decoded frame, and drops any previous ones.\n"
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" --rotation value\n" " --rotation value\n"
" Set the initial display rotation.\n" " Set the initial display rotation.\n"
" Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n" " Possibles values are 0, 1, 2 and 3. Each increment adds a 90\n"
@ -176,9 +176,13 @@ scrcpy_print_usage(const char *arg0) {
" on exit.\n" " on exit.\n"
" It only shows physical touches (not clicks from scrcpy).\n" " It only shows physical touches (not clicks from scrcpy).\n"
"\n" "\n"
" -v, --version\n" #ifdef HAVE_V4L2
" Print the version of scrcpy.\n" " --v4l2-sink /dev/videoN\n"
" Output to v4l2loopback device.\n"
" It requires to lock the video orientation (see\n"
" --lock-video-orientation).\n"
"\n" "\n"
#endif
" -V, --verbosity value\n" " -V, --verbosity value\n"
" Set the log level (debug, info, warn or error).\n" " Set the log level (debug, info, warn or error).\n"
#ifndef NDEBUG #ifndef NDEBUG
@ -186,6 +190,9 @@ scrcpy_print_usage(const char *arg0) {
#else #else
" Default is info.\n" " Default is info.\n"
#endif #endif
"\n"
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n" "\n"
" -w, --stay-awake\n" " -w, --stay-awake\n"
" Keep the device on while scrcpy is running, when the device\n" " Keep the device on while scrcpy is running, when the device\n"
@ -298,12 +305,7 @@ scrcpy_print_usage(const char *arg0) {
"\n" "\n"
" Drag & drop APK file\n" " Drag & drop APK file\n"
" Install APK from computer\n" " Install APK from computer\n"
"\n", "\n", arg0);
arg0,
DEFAULT_BIT_RATE,
DEFAULT_LOCK_VIDEO_ORIENTATION, DEFAULT_LOCK_VIDEO_ORIENTATION >= 0 ? "" : " (unlocked)",
DEFAULT_MAX_SIZE, DEFAULT_MAX_SIZE ? "" : " (unlimited)",
DEFAULT_LOCAL_PORT_RANGE_FIRST, DEFAULT_LOCAL_PORT_RANGE_LAST);
} }
static bool static bool
@ -391,15 +393,27 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
} }
static bool static bool
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) { parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
long value; long value;
bool ok = parse_integer_arg(s, &value, false, -1, 3, bool ok = parse_integer_arg(s, &value, false, 0, 3,
"lock video orientation"); "lock video orientation");
if (!ok) { if (!ok) {
return false; return false;
} }
*lock_video_orientation = (int8_t) value; *lock_mode = (enum sc_lock_video_orientation) value;
return true; return true;
} }
@ -478,14 +492,14 @@ parse_port_range(const char *s, struct sc_port_range *port_range) {
} }
static bool static bool
parse_display_id(const char *s, uint16_t *display_id) { parse_display_id(const char *s, uint32_t *display_id) {
long value; long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "display id"); bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "display id");
if (!ok) { if (!ok) {
return false; return false;
} }
*display_id = (uint16_t) value; *display_id = (uint32_t) value;
return true; return true;
} }
@ -668,6 +682,8 @@ guess_record_format(const char *filename) {
#define OPT_FORWARD_ALL_CLICKS 1023 #define OPT_FORWARD_ALL_CLICKS 1023
#define OPT_LEGACY_PASTE 1024 #define OPT_LEGACY_PASTE 1024
#define OPT_ENCODER_NAME 1025 #define OPT_ENCODER_NAME 1025
#define OPT_POWER_OFF_ON_CLOSE 1026
#define OPT_V4L2_SINK 1027
bool bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) { scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
@ -687,7 +703,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"fullscreen", no_argument, NULL, 'f'}, {"fullscreen", no_argument, NULL, 'f'},
{"help", no_argument, NULL, 'h'}, {"help", no_argument, NULL, 'h'},
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE}, {"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
{"lock-video-orientation", required_argument, NULL, {"lock-video-orientation", optional_argument, NULL,
OPT_LOCK_VIDEO_ORIENTATION}, OPT_LOCK_VIDEO_ORIENTATION},
{"max-fps", required_argument, NULL, OPT_MAX_FPS}, {"max-fps", required_argument, NULL, OPT_MAX_FPS},
{"max-size", required_argument, NULL, 'm'}, {"max-size", required_argument, NULL, 'm'},
@ -709,6 +725,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"show-touches", no_argument, NULL, 't'}, {"show-touches", no_argument, NULL, 't'},
{"stay-awake", no_argument, NULL, 'w'}, {"stay-awake", no_argument, NULL, 'w'},
{"turn-screen-off", no_argument, NULL, 'S'}, {"turn-screen-off", no_argument, NULL, 'S'},
#ifdef HAVE_V4L2
{"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK},
#endif
{"verbosity", required_argument, NULL, 'V'}, {"verbosity", required_argument, NULL, 'V'},
{"version", no_argument, NULL, 'v'}, {"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE}, {"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
@ -718,6 +737,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
{"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT}, {"window-height", required_argument, NULL, OPT_WINDOW_HEIGHT},
{"window-borderless", no_argument, NULL, {"window-borderless", no_argument, NULL,
OPT_WINDOW_BORDERLESS}, OPT_WINDOW_BORDERLESS},
{"power-off-on-close", no_argument, NULL,
OPT_POWER_OFF_ON_CLOSE},
{NULL, 0, NULL, 0 }, {NULL, 0, NULL, 0 },
}; };
@ -770,7 +791,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
} }
break; break;
case OPT_LOCK_VIDEO_ORIENTATION: case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) { if (!parse_lock_video_orientation(optarg,
&opts->lock_video_orientation)) {
return false; return false;
} }
break; break;
@ -815,7 +837,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
opts->stay_awake = true; opts->stay_awake = true;
break; break;
case OPT_RENDER_EXPIRED_FRAMES: case OPT_RENDER_EXPIRED_FRAMES:
opts->render_expired_frames = true; LOGW("Option --render-expired-frames has been removed. This "
"flag has been ignored.");
break; break;
case OPT_WINDOW_TITLE: case OPT_WINDOW_TITLE:
opts->window_title = optarg; opts->window_title = optarg;
@ -886,16 +909,39 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
case OPT_LEGACY_PASTE: case OPT_LEGACY_PASTE:
opts->legacy_paste = true; opts->legacy_paste = true;
break; break;
case OPT_POWER_OFF_ON_CLOSE:
opts->power_off_on_close = true;
break;
#ifdef HAVE_V4L2
case OPT_V4L2_SINK:
opts->v4l2_device = optarg;
break;
#endif
default: default:
// getopt prints the error message on stderr // getopt prints the error message on stderr
return false; return false;
} }
} }
#ifdef HAVE_V4L2
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;
}
if (opts->v4l2_device && opts->lock_video_orientation
== SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
#else
if (!opts->display && !opts->record_filename) { if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)"); LOGE("-N/--no-display requires screen recording (-r/--record)");
return false; return false;
} }
#endif
int index = optind; int index = optind;
if (index < argc) { if (index < argc) {

View File

@ -1,9 +1,10 @@
#ifndef SCRCPY_CLI_H #ifndef SCRCPY_CLI_H
#define SCRCPY_CLI_H #define SCRCPY_CLI_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include "config.h"
#include "scrcpy.h" #include "scrcpy.h"
struct scrcpy_cli_args { struct scrcpy_cli_args {

View File

@ -1,35 +1,14 @@
#ifndef COMMON_H #ifndef COMMON_H
#define COMMON_H #define COMMON_H
#include <stdint.h>
#include "config.h" #include "config.h"
#include "compat.h"
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
#define MIN(X,Y) (X) < (Y) ? (X) : (Y) #define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y) #define MAX(X,Y) (X) > (Y) ? (X) : (Y)
struct size { #define container_of(ptr, type, member) \
uint16_t width; ((type *) (((char *) (ptr)) - offsetof(type, member)))
uint16_t height;
};
struct point {
int32_t x;
int32_t y;
};
struct position {
// The video screen size may be different from the real device screen size,
// so store to which size the absolute position apply, to scale it
// accordingly.
struct size screen_size;
struct point point;
};
struct port_range {
uint16_t first;
uint16_t last;
};
#endif #endif

14
app/src/compat.c Normal file
View File

@ -0,0 +1,14 @@
#include "compat.h"
#include "config.h"
#ifndef HAVE_STRDUP
char *strdup(const char *s) {
size_t size = strlen(s) + 1;
char *dup = malloc(size);
if (dup) {
memcpy(dup, s, size);
}
return dup;
}
#endif

View File

@ -1,19 +1,16 @@
#ifndef COMPAT_H #ifndef COMPAT_H
#define COMPAT_H #define COMPAT_H
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#define _GNU_SOURCE
#ifdef __APPLE__
# define _DARWIN_C_SOURCE
#endif
#include <libavformat/version.h> #include <libavformat/version.h>
#include <SDL2/SDL_version.h> #include <SDL2/SDL_version.h>
// In ffmpeg/doc/APIchanges:
// 2016-04-11 - 6f69f7a / 9200514 - lavf 57.33.100 / 57.5.0 - avformat.h
// Add AVStream.codecpar, deprecate AVStream.codec.
#if (LIBAVFORMAT_VERSION_MICRO >= 100 /* FFmpeg */ && \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 33, 100)) \
|| (LIBAVFORMAT_VERSION_MICRO < 100 && /* Libav */ \
LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 5, 0))
# define SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
#endif
// In ffmpeg/doc/APIchanges: // In ffmpeg/doc/APIchanges:
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h // 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
// Deprecate use of av_register_input_format(), av_register_output_format(), // Deprecate use of av_register_input_format(), av_register_output_format(),
@ -25,15 +22,6 @@
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif #endif
// In ffmpeg/doc/APIchanges:
// 2016-04-21 - 7fc329e - lavc 57.37.100 - avcodec.h
// Add a new audio/video encoding and decoding API with decoupled input
// and output -- avcodec_send_packet(), avcodec_receive_frame(),
// avcodec_send_frame() and avcodec_receive_packet().
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 100)
# define SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
#endif
#if SDL_VERSION_ATLEAST(2, 0, 5) #if SDL_VERSION_ATLEAST(2, 0, 5)
// <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH> // <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH>
# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH # define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
@ -48,4 +36,8 @@
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR # define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
#endif #endif
#ifndef HAVE_STRDUP
char *strdup(const char *s);
#endif
#endif #endif

View File

@ -1,9 +1,9 @@
#include "control_msg.h" #include "control_msg.h"
#include <assert.h> #include <assert.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "config.h"
#include "util/buffer_util.h" #include "util/buffer_util.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
@ -67,6 +67,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buffer_write32be(&buf[17], buffer_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll); (uint32_t) msg->inject_scroll_event.vscroll);
return 21; return 21;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->inject_keycode.action;
return 2;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: { case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
buf[1] = !!msg->set_clipboard.paste; buf[1] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text, size_t len = write_string(msg->set_clipboard.text,
@ -77,9 +80,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case CONTROL_MSG_TYPE_GET_CLIPBOARD: case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE: case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data // no additional data
@ -94,10 +97,10 @@ void
control_msg_destroy(struct control_msg *msg) { control_msg_destroy(struct control_msg *msg) {
switch (msg->type) { switch (msg->type) {
case CONTROL_MSG_TYPE_INJECT_TEXT: case CONTROL_MSG_TYPE_INJECT_TEXT:
SDL_free(msg->inject_text.text); free(msg->inject_text.text);
break; break;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: case CONTROL_MSG_TYPE_SET_CLIPBOARD:
SDL_free(msg->set_clipboard.text); free(msg->set_clipboard.text);
break; break;
default: default:
// do nothing // do nothing

View File

@ -1,14 +1,15 @@
#ifndef CONTROLMSG_H #ifndef CONTROLMSG_H
#define CONTROLMSG_H #define CONTROLMSG_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "config.h"
#include "android/input.h" #include "android/input.h"
#include "android/keycodes.h" #include "android/keycodes.h"
#include "common.h" #include "coords.h"
#define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k #define CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
@ -26,7 +27,8 @@ enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
CONTROL_MSG_TYPE_GET_CLIPBOARD, CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
@ -49,7 +51,7 @@ struct control_msg {
enum android_metastate metastate; enum android_metastate metastate;
} inject_keycode; } inject_keycode;
struct { struct {
char *text; // owned, to be freed by SDL_free() char *text; // owned, to be freed by free()
} inject_text; } inject_text;
struct { struct {
enum android_motionevent_action action; enum android_motionevent_action action;
@ -64,7 +66,11 @@ struct control_msg {
int32_t vscroll; int32_t vscroll;
} inject_scroll_event; } inject_scroll_event;
struct { struct {
char *text; // owned, to be freed by SDL_free() enum android_keyevent_action action; // action for the BACK key
// screen may only be turned on on ACTION_DOWN
} back_or_screen_on;
struct {
char *text; // owned, to be freed by free()
bool paste; bool paste;
} set_clipboard; } set_clipboard;
struct { struct {

View File

@ -2,26 +2,27 @@
#include <assert.h> #include <assert.h>
#include "config.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
bool bool
controller_init(struct controller *controller, socket_t control_socket) { controller_init(struct controller *controller, socket_t control_socket) {
cbuf_init(&controller->queue); cbuf_init(&controller->queue);
if (!receiver_init(&controller->receiver, control_socket)) { bool ok = receiver_init(&controller->receiver, control_socket);
if (!ok) {
return false; return false;
} }
if (!(controller->mutex = SDL_CreateMutex())) { ok = sc_mutex_init(&controller->mutex);
if (!ok) {
receiver_destroy(&controller->receiver); receiver_destroy(&controller->receiver);
return false; return false;
} }
if (!(controller->msg_cond = SDL_CreateCond())) { ok = sc_cond_init(&controller->msg_cond);
if (!ok) {
receiver_destroy(&controller->receiver); receiver_destroy(&controller->receiver);
SDL_DestroyMutex(controller->mutex); sc_mutex_destroy(&controller->mutex);
return false; return false;
} }
@ -33,8 +34,8 @@ controller_init(struct controller *controller, socket_t control_socket) {
void void
controller_destroy(struct controller *controller) { controller_destroy(struct controller *controller) {
SDL_DestroyCond(controller->msg_cond); sc_cond_destroy(&controller->msg_cond);
SDL_DestroyMutex(controller->mutex); sc_mutex_destroy(&controller->mutex);
struct control_msg msg; struct control_msg msg;
while (cbuf_take(&controller->queue, &msg)) { while (cbuf_take(&controller->queue, &msg)) {
@ -47,13 +48,13 @@ controller_destroy(struct controller *controller) {
bool bool
controller_push_msg(struct controller *controller, controller_push_msg(struct controller *controller,
const struct control_msg *msg) { const struct control_msg *msg) {
mutex_lock(controller->mutex); sc_mutex_lock(&controller->mutex);
bool was_empty = cbuf_is_empty(&controller->queue); bool was_empty = cbuf_is_empty(&controller->queue);
bool res = cbuf_push(&controller->queue, *msg); bool res = cbuf_push(&controller->queue, *msg);
if (was_empty) { if (was_empty) {
cond_signal(controller->msg_cond); sc_cond_signal(&controller->msg_cond);
} }
mutex_unlock(controller->mutex); sc_mutex_unlock(&controller->mutex);
return res; return res;
} }
@ -61,12 +62,12 @@ static bool
process_msg(struct controller *controller, process_msg(struct controller *controller,
const struct control_msg *msg) { const struct control_msg *msg) {
static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE]; static unsigned char serialized_msg[CONTROL_MSG_MAX_SIZE];
int length = control_msg_serialize(msg, serialized_msg); size_t length = control_msg_serialize(msg, serialized_msg);
if (!length) { if (!length) {
return false; return false;
} }
int w = net_send_all(controller->control_socket, serialized_msg, length); int w = net_send_all(controller->control_socket, serialized_msg, length);
return w == length; return (size_t) w == length;
} }
static int static int
@ -74,20 +75,20 @@ run_controller(void *data) {
struct controller *controller = data; struct controller *controller = data;
for (;;) { for (;;) {
mutex_lock(controller->mutex); sc_mutex_lock(&controller->mutex);
while (!controller->stopped && cbuf_is_empty(&controller->queue)) { while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
cond_wait(controller->msg_cond, controller->mutex); sc_cond_wait(&controller->msg_cond, &controller->mutex);
} }
if (controller->stopped) { if (controller->stopped) {
// stop immediately, do not process further msgs // stop immediately, do not process further msgs
mutex_unlock(controller->mutex); sc_mutex_unlock(&controller->mutex);
break; break;
} }
struct control_msg msg; struct control_msg msg;
bool non_empty = cbuf_take(&controller->queue, &msg); bool non_empty = cbuf_take(&controller->queue, &msg);
assert(non_empty); assert(non_empty);
(void) non_empty; (void) non_empty;
mutex_unlock(controller->mutex); sc_mutex_unlock(&controller->mutex);
bool ok = process_msg(controller, &msg); bool ok = process_msg(controller, &msg);
control_msg_destroy(&msg); control_msg_destroy(&msg);
@ -103,16 +104,16 @@ bool
controller_start(struct controller *controller) { controller_start(struct controller *controller) {
LOGD("Starting controller thread"); LOGD("Starting controller thread");
controller->thread = SDL_CreateThread(run_controller, "controller", bool ok = sc_thread_create(&controller->thread, run_controller,
controller); "controller", controller);
if (!controller->thread) { if (!ok) {
LOGC("Could not start controller thread"); LOGC("Could not start controller thread");
return false; return false;
} }
if (!receiver_start(&controller->receiver)) { if (!receiver_start(&controller->receiver)) {
controller_stop(controller); controller_stop(controller);
SDL_WaitThread(controller->thread, NULL); sc_thread_join(&controller->thread, NULL);
return false; return false;
} }
@ -121,14 +122,14 @@ controller_start(struct controller *controller) {
void void
controller_stop(struct controller *controller) { controller_stop(struct controller *controller) {
mutex_lock(controller->mutex); sc_mutex_lock(&controller->mutex);
controller->stopped = true; controller->stopped = true;
cond_signal(controller->msg_cond); sc_cond_signal(&controller->msg_cond);
mutex_unlock(controller->mutex); sc_mutex_unlock(&controller->mutex);
} }
void void
controller_join(struct controller *controller) { controller_join(struct controller *controller) {
SDL_WaitThread(controller->thread, NULL); sc_thread_join(&controller->thread, NULL);
receiver_join(&controller->receiver); receiver_join(&controller->receiver);
} }

View File

@ -1,23 +1,23 @@
#ifndef CONTROLLER_H #ifndef CONTROLLER_H
#define CONTROLLER_H #define CONTROLLER_H
#include <stdbool.h> #include "common.h"
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <stdbool.h>
#include "config.h"
#include "control_msg.h" #include "control_msg.h"
#include "receiver.h" #include "receiver.h"
#include "util/cbuf.h" #include "util/cbuf.h"
#include "util/net.h" #include "util/net.h"
#include "util/thread.h"
struct control_msg_queue CBUF(struct control_msg, 64); struct control_msg_queue CBUF(struct control_msg, 64);
struct controller { struct controller {
socket_t control_socket; socket_t control_socket;
SDL_Thread *thread; sc_thread thread;
SDL_mutex *mutex; sc_mutex mutex;
SDL_cond *msg_cond; sc_cond msg_cond;
bool stopped; bool stopped;
struct control_msg_queue queue; struct control_msg_queue queue;
struct receiver receiver; struct receiver receiver;

24
app/src/coords.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef SC_COORDS
#define SC_COORDS
#include <stdint.h>
struct size {
uint16_t width;
uint16_t height;
};
struct point {
int32_t x;
int32_t y;
};
struct position {
// The video screen size may be different from the real device screen size,
// so store to which size the absolute position apply, to scale it
// accordingly.
struct size screen_size;
struct point point;
};
#endif

View File

@ -1,42 +1,43 @@
#include "decoder.h" #include "decoder.h"
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h>
#include "config.h"
#include "compat.h"
#include "events.h" #include "events.h"
#include "recorder.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/buffer_util.h" #include "trait/frame_sink.h"
#include "util/log.h" #include "util/log.h"
// set the decoded frame as ready for rendering, and notify /** Downcast packet_sink to decoder */
#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink)
static void static void
push_frame(struct decoder *decoder) { decoder_close_first_sinks(struct decoder *decoder, unsigned count) {
bool previous_frame_skipped; while (count) {
video_buffer_offer_decoded_frame(decoder->video_buffer, struct sc_frame_sink *sink = decoder->sinks[--count];
&previous_frame_skipped); sink->ops->close(sink);
if (previous_frame_skipped) {
// the previous EVENT_NEW_FRAME will consume this frame
return;
} }
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
};
SDL_PushEvent(&new_frame_event);
} }
void static inline void
decoder_init(struct decoder *decoder, struct video_buffer *vb) { decoder_close_sinks(struct decoder *decoder) {
decoder->video_buffer = vb; decoder_close_first_sinks(decoder, decoder->sink_count);
} }
bool static bool
decoder_open_sinks(struct decoder *decoder) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->open(sink)) {
LOGE("Could not open frame sink %d", i);
decoder_close_first_sinks(decoder, i);
return false;
}
}
return true;
}
static bool
decoder_open(struct decoder *decoder, const AVCodec *codec) { decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec); decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) { if (!decoder->codec_ctx) {
@ -50,52 +51,108 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) {
return false; return false;
} }
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
LOGE("Could not create decoder frame");
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
if (!decoder_open_sinks(decoder)) {
LOGE("Could not open decoder sinks");
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
return false;
}
return true; return true;
} }
void static void
decoder_close(struct decoder *decoder) { decoder_close(struct decoder *decoder) {
decoder_close_sinks(decoder);
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx); avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx); avcodec_free_context(&decoder->codec_ctx);
} }
bool static bool
push_frame_to_sinks(struct decoder *decoder, const AVFrame *frame) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->push(sink, frame)) {
LOGE("Could not send frame to sink %d", i);
return false;
}
}
return true;
}
static bool
decoder_push(struct decoder *decoder, const AVPacket *packet) { decoder_push(struct decoder *decoder, const AVPacket *packet) {
// the new decoding/encoding API has been introduced by: bool is_config = packet->pts == AV_NOPTS_VALUE;
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726> if (is_config) {
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API // nothing to do
int ret; return true;
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) { }
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Could not send video packet: %d", ret); LOGE("Could not send video packet: %d", ret);
return false; return false;
} }
ret = avcodec_receive_frame(decoder->codec_ctx, ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
decoder->video_buffer->decoding_frame);
if (!ret) { if (!ret) {
// a frame was received // a frame was received
push_frame(decoder); bool ok = push_frame_to_sinks(decoder, decoder->frame);
// A frame lost should not make the whole pipeline fail. The error, if
// any, is already logged.
(void) ok;
av_frame_unref(decoder->frame);
} else if (ret != AVERROR(EAGAIN)) { } else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret); LOGE("Could not receive video frame: %d", ret);
return false; return false;
} }
#else
int got_picture;
int len = avcodec_decode_video2(decoder->codec_ctx,
decoder->video_buffer->decoding_frame,
&got_picture,
packet);
if (len < 0) {
LOGE("Could not decode video packet: %d", len);
return false;
}
if (got_picture) {
push_frame(decoder);
}
#endif
return true; return true;
} }
void static bool
decoder_interrupt(struct decoder *decoder) { decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
video_buffer_interrupt(decoder->video_buffer); struct decoder *decoder = DOWNCAST(sink);
return decoder_open(decoder, codec);
}
static void
decoder_packet_sink_close(struct sc_packet_sink *sink) {
struct decoder *decoder = DOWNCAST(sink);
decoder_close(decoder);
}
static bool
decoder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) {
struct decoder *decoder = DOWNCAST(sink);
return decoder_push(decoder, packet);
}
void
decoder_init(struct decoder *decoder) {
static const struct sc_packet_sink_ops ops = {
.open = decoder_packet_sink_open,
.close = decoder_packet_sink_close,
.push = decoder_packet_sink_push,
};
decoder->packet_sink.ops = &ops;
}
void
decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink) {
assert(decoder->sink_count < DECODER_MAX_SINKS);
assert(sink);
assert(sink->ops);
decoder->sinks[decoder->sink_count++] = sink;
} }

View File

@ -1,31 +1,29 @@
#ifndef DECODER_H #ifndef DECODER_H
#define DECODER_H #define DECODER_H
#include "common.h"
#include "trait/packet_sink.h"
#include <stdbool.h> #include <stdbool.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include "config.h" #define DECODER_MAX_SINKS 2
struct video_buffer;
struct decoder { struct decoder {
struct video_buffer *video_buffer; struct sc_packet_sink packet_sink; // packet sink trait
struct sc_frame_sink *sinks[DECODER_MAX_SINKS];
unsigned sink_count;
AVCodecContext *codec_ctx; AVCodecContext *codec_ctx;
AVFrame *frame;
}; };
void void
decoder_init(struct decoder *decoder, struct video_buffer *vb); decoder_init(struct decoder *decoder);
bool
decoder_open(struct decoder *decoder, const AVCodec *codec);
void void
decoder_close(struct decoder *decoder); decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink);
bool
decoder_push(struct decoder *decoder, const AVPacket *packet);
void
decoder_interrupt(struct decoder *decoder);
#endif #endif

View File

@ -1,6 +1,5 @@
#include "device.h" #include "device.h"
#include "config.h"
#include "util/log.h" #include "util/log.h"
bool bool

View File

@ -1,10 +1,11 @@
#ifndef DEVICE_H #ifndef DEVICE_H
#define DEVICE_H #define DEVICE_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include "config.h" #include "coords.h"
#include "common.h"
#include "util/net.h" #include "util/net.h"
#define DEVICE_NAME_FIELD_LENGTH 64 #define DEVICE_NAME_FIELD_LENGTH 64

View File

@ -1,8 +1,8 @@
#include "device_msg.h" #include "device_msg.h"
#include <stdlib.h>
#include <string.h> #include <string.h>
#include "config.h"
#include "util/buffer_util.h" #include "util/buffer_util.h"
#include "util/log.h" #include "util/log.h"
@ -21,7 +21,7 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
if (clipboard_len > len - 5) { if (clipboard_len > len - 5) {
return 0; // not available return 0; // not available
} }
char *text = SDL_malloc(clipboard_len + 1); char *text = malloc(clipboard_len + 1);
if (!text) { if (!text) {
LOGW("Could not allocate text for clipboard"); LOGW("Could not allocate text for clipboard");
return -1; return -1;
@ -43,6 +43,6 @@ device_msg_deserialize(const unsigned char *buf, size_t len,
void void
device_msg_destroy(struct device_msg *msg) { device_msg_destroy(struct device_msg *msg) {
if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) { if (msg->type == DEVICE_MSG_TYPE_CLIPBOARD) {
SDL_free(msg->clipboard.text); free(msg->clipboard.text);
} }
} }

View File

@ -1,12 +1,12 @@
#ifndef DEVICEMSG_H #ifndef DEVICEMSG_H
#define DEVICEMSG_H #define DEVICEMSG_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <unistd.h> #include <unistd.h>
#include "config.h"
#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
// type: 1 byte; length: 4 bytes // type: 1 byte; length: 4 bytes
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5) #define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
@ -19,7 +19,7 @@ struct device_msg {
enum device_msg_type type; enum device_msg_type type;
union { union {
struct { struct {
char *text; // owned, to be freed by SDL_free() char *text; // owned, to be freed by free()
} clipboard; } clipboard;
}; };
}; };

View File

@ -1,7 +1,5 @@
#include "event_converter.h" #include "event_converter.h"
#include "config.h"
#define MAP(FROM, TO) case FROM: *to = TO; return true #define MAP(FROM, TO) case FROM: *to = TO; return true
#define FAIL default: return false #define FAIL default: return false

View File

@ -1,10 +1,11 @@
#ifndef CONVERT_H #ifndef CONVERT_H
#define CONVERT_H #define CONVERT_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include "config.h"
#include "control_msg.h" #include "control_msg.h"
bool bool

View File

@ -1,3 +1,2 @@
#define EVENT_NEW_SESSION SDL_USEREVENT #define EVENT_NEW_FRAME SDL_USEREVENT
#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) #define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 2)

View File

@ -3,16 +3,14 @@
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include "config.h" #include "adb.h"
#include "command.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/" #define DEFAULT_PUSH_TARGET "/sdcard/"
static void static void
file_handler_request_destroy(struct file_handler_request *req) { file_handler_request_destroy(struct file_handler_request *req) {
SDL_free(req->file); free(req->file);
} }
bool bool
@ -21,21 +19,23 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
cbuf_init(&file_handler->queue); cbuf_init(&file_handler->queue);
if (!(file_handler->mutex = SDL_CreateMutex())) { bool ok = sc_mutex_init(&file_handler->mutex);
if (!ok) {
return false; return false;
} }
if (!(file_handler->event_cond = SDL_CreateCond())) { ok = sc_cond_init(&file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); if (!ok) {
sc_mutex_destroy(&file_handler->mutex);
return false; return false;
} }
if (serial) { if (serial) {
file_handler->serial = SDL_strdup(serial); file_handler->serial = strdup(serial);
if (!file_handler->serial) { if (!file_handler->serial) {
LOGW("Could not strdup serial"); LOGW("Could not strdup serial");
SDL_DestroyCond(file_handler->event_cond); sc_cond_destroy(&file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); sc_mutex_destroy(&file_handler->mutex);
return false; return false;
} }
} else { } else {
@ -55,9 +55,9 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
void void
file_handler_destroy(struct file_handler *file_handler) { file_handler_destroy(struct file_handler *file_handler) {
SDL_DestroyCond(file_handler->event_cond); sc_cond_destroy(&file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex); sc_mutex_destroy(&file_handler->mutex);
SDL_free(file_handler->serial); free(file_handler->serial);
struct file_handler_request req; struct file_handler_request req;
while (cbuf_take(&file_handler->queue, &req)) { while (cbuf_take(&file_handler->queue, &req)) {
@ -93,13 +93,13 @@ file_handler_request(struct file_handler *file_handler,
.file = file, .file = file,
}; };
mutex_lock(file_handler->mutex); sc_mutex_lock(&file_handler->mutex);
bool was_empty = cbuf_is_empty(&file_handler->queue); bool was_empty = cbuf_is_empty(&file_handler->queue);
bool res = cbuf_push(&file_handler->queue, req); bool res = cbuf_push(&file_handler->queue, req);
if (was_empty) { if (was_empty) {
cond_signal(file_handler->event_cond); sc_cond_signal(&file_handler->event_cond);
} }
mutex_unlock(file_handler->mutex); sc_mutex_unlock(&file_handler->mutex);
return res; return res;
} }
@ -108,14 +108,14 @@ run_file_handler(void *data) {
struct file_handler *file_handler = data; struct file_handler *file_handler = data;
for (;;) { for (;;) {
mutex_lock(file_handler->mutex); sc_mutex_lock(&file_handler->mutex);
file_handler->current_process = PROCESS_NONE; file_handler->current_process = PROCESS_NONE;
while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) { while (!file_handler->stopped && cbuf_is_empty(&file_handler->queue)) {
cond_wait(file_handler->event_cond, file_handler->mutex); sc_cond_wait(&file_handler->event_cond, &file_handler->mutex);
} }
if (file_handler->stopped) { if (file_handler->stopped) {
// stop immediately, do not process further events // stop immediately, do not process further events
mutex_unlock(file_handler->mutex); sc_mutex_unlock(&file_handler->mutex);
break; break;
} }
struct file_handler_request req; struct file_handler_request req;
@ -133,16 +133,16 @@ run_file_handler(void *data) {
file_handler->push_target); file_handler->push_target);
} }
file_handler->current_process = process; file_handler->current_process = process;
mutex_unlock(file_handler->mutex); sc_mutex_unlock(&file_handler->mutex);
if (req.action == ACTION_INSTALL_APK) { if (req.action == ACTION_INSTALL_APK) {
if (process_check_success(process, "adb install")) { if (process_check_success(process, "adb install", false)) {
LOGI("%s successfully installed", req.file); LOGI("%s successfully installed", req.file);
} else { } else {
LOGE("Failed to install %s", req.file); LOGE("Failed to install %s", req.file);
} }
} else { } else {
if (process_check_success(process, "adb push")) { if (process_check_success(process, "adb push", false)) {
LOGI("%s successfully pushed to %s", req.file, LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target); file_handler->push_target);
} else { } else {
@ -151,6 +151,14 @@ run_file_handler(void *data) {
} }
} }
sc_mutex_lock(&file_handler->mutex);
// Close the process (it is necessary already terminated)
// Execute this call with mutex locked to avoid race conditions with
// file_handler_stop()
process_close(file_handler->current_process);
file_handler->current_process = PROCESS_NONE;
sc_mutex_unlock(&file_handler->mutex);
file_handler_request_destroy(&req); file_handler_request_destroy(&req);
} }
return 0; return 0;
@ -160,9 +168,9 @@ bool
file_handler_start(struct file_handler *file_handler) { file_handler_start(struct file_handler *file_handler) {
LOGD("Starting file_handler thread"); LOGD("Starting file_handler thread");
file_handler->thread = SDL_CreateThread(run_file_handler, "file_handler", bool ok = sc_thread_create(&file_handler->thread, run_file_handler,
file_handler); "file_handler", file_handler);
if (!file_handler->thread) { if (!ok) {
LOGC("Could not start file_handler thread"); LOGC("Could not start file_handler thread");
return false; return false;
} }
@ -172,20 +180,18 @@ file_handler_start(struct file_handler *file_handler) {
void void
file_handler_stop(struct file_handler *file_handler) { file_handler_stop(struct file_handler *file_handler) {
mutex_lock(file_handler->mutex); sc_mutex_lock(&file_handler->mutex);
file_handler->stopped = true; file_handler->stopped = true;
cond_signal(file_handler->event_cond); sc_cond_signal(&file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) { if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) { if (!process_terminate(file_handler->current_process)) {
LOGW("Could not terminate install process"); LOGW("Could not terminate push/install process");
} }
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;
} }
mutex_unlock(file_handler->mutex); sc_mutex_unlock(&file_handler->mutex);
} }
void void
file_handler_join(struct file_handler *file_handler) { file_handler_join(struct file_handler *file_handler) {
SDL_WaitThread(file_handler->thread, NULL); sc_thread_join(&file_handler->thread, NULL);
} }

View File

@ -1,13 +1,13 @@
#ifndef FILE_HANDLER_H #ifndef FILE_HANDLER_H
#define FILE_HANDLER_H #define FILE_HANDLER_H
#include <stdbool.h> #include "common.h"
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include <stdbool.h>
#include "command.h"
#include "adb.h"
#include "util/cbuf.h" #include "util/cbuf.h"
#include "util/thread.h"
typedef enum { typedef enum {
ACTION_INSTALL_APK, ACTION_INSTALL_APK,
@ -24,9 +24,9 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler { struct file_handler {
char *serial; char *serial;
const char *push_target; const char *push_target;
SDL_Thread *thread; sc_thread thread;
SDL_mutex *mutex; sc_mutex mutex;
SDL_cond *event_cond; sc_cond event_cond;
bool stopped; bool stopped;
bool initialized; bool initialized;
process_t current_process; process_t current_process;
@ -49,7 +49,7 @@ file_handler_stop(struct file_handler *file_handler);
void void
file_handler_join(struct file_handler *file_handler); file_handler_join(struct file_handler *file_handler);
// take ownership of file, and will SDL_free() it // take ownership of file, and will free() it
bool bool
file_handler_request(struct file_handler *file_handler, file_handler_request(struct file_handler *file_handler,
file_handler_action_t action, file_handler_action_t action,

View File

@ -3,26 +3,24 @@
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include "config.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
#define FPS_COUNTER_INTERVAL_MS 1000 #define FPS_COUNTER_INTERVAL_MS 1000
bool bool
fps_counter_init(struct fps_counter *counter) { fps_counter_init(struct fps_counter *counter) {
counter->mutex = SDL_CreateMutex(); bool ok = sc_mutex_init(&counter->mutex);
if (!counter->mutex) { if (!ok) {
return false; return false;
} }
counter->state_cond = SDL_CreateCond(); ok = sc_cond_init(&counter->state_cond);
if (!counter->state_cond) { if (!ok) {
SDL_DestroyMutex(counter->mutex); sc_mutex_destroy(&counter->mutex);
return false; return false;
} }
counter->thread = NULL; counter->thread_started = false;
atomic_init(&counter->started, 0); atomic_init(&counter->started, 0);
// no need to initialize the other fields, they are unused until started // no need to initialize the other fields, they are unused until started
@ -31,8 +29,8 @@ fps_counter_init(struct fps_counter *counter) {
void void
fps_counter_destroy(struct fps_counter *counter) { fps_counter_destroy(struct fps_counter *counter) {
SDL_DestroyCond(counter->state_cond); sc_cond_destroy(&counter->state_cond);
SDL_DestroyMutex(counter->mutex); sc_mutex_destroy(&counter->mutex);
} }
static inline bool static inline bool
@ -78,10 +76,10 @@ static int
run_fps_counter(void *data) { run_fps_counter(void *data) {
struct fps_counter *counter = data; struct fps_counter *counter = data;
mutex_lock(counter->mutex); sc_mutex_lock(&counter->mutex);
while (!counter->interrupted) { while (!counter->interrupted) {
while (!counter->interrupted && !is_started(counter)) { while (!counter->interrupted && !is_started(counter)) {
cond_wait(counter->state_cond, counter->mutex); sc_cond_wait(&counter->state_cond, &counter->mutex);
} }
while (!counter->interrupted && is_started(counter)) { while (!counter->interrupted && is_started(counter)) {
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
@ -91,32 +89,35 @@ run_fps_counter(void *data) {
uint32_t remaining = counter->next_timestamp - now; uint32_t remaining = counter->next_timestamp - now;
// ignore the reason (timeout or signaled), we just loop anyway // ignore the reason (timeout or signaled), we just loop anyway
cond_wait_timeout(counter->state_cond, counter->mutex, remaining); sc_cond_timedwait(&counter->state_cond, &counter->mutex, remaining);
} }
} }
mutex_unlock(counter->mutex); sc_mutex_unlock(&counter->mutex);
return 0; return 0;
} }
bool bool
fps_counter_start(struct fps_counter *counter) { fps_counter_start(struct fps_counter *counter) {
mutex_lock(counter->mutex); sc_mutex_lock(&counter->mutex);
counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS; counter->next_timestamp = SDL_GetTicks() + FPS_COUNTER_INTERVAL_MS;
counter->nr_rendered = 0; counter->nr_rendered = 0;
counter->nr_skipped = 0; counter->nr_skipped = 0;
mutex_unlock(counter->mutex); sc_mutex_unlock(&counter->mutex);
set_started(counter, true); set_started(counter, true);
cond_signal(counter->state_cond); sc_cond_signal(&counter->state_cond);
// counter->thread is always accessed from the same thread, no need to lock // counter->thread_started and counter->thread are always accessed from the
if (!counter->thread) { // same thread, no need to lock
counter->thread = if (!counter->thread_started) {
SDL_CreateThread(run_fps_counter, "fps counter", counter); bool ok = sc_thread_create(&counter->thread, run_fps_counter,
if (!counter->thread) { "fps counter", counter);
if (!ok) {
LOGE("Could not start FPS counter thread"); LOGE("Could not start FPS counter thread");
return false; return false;
} }
counter->thread_started = true;
} }
return true; return true;
@ -125,7 +126,7 @@ fps_counter_start(struct fps_counter *counter) {
void void
fps_counter_stop(struct fps_counter *counter) { fps_counter_stop(struct fps_counter *counter) {
set_started(counter, false); set_started(counter, false);
cond_signal(counter->state_cond); sc_cond_signal(&counter->state_cond);
} }
bool bool
@ -135,21 +136,21 @@ fps_counter_is_started(struct fps_counter *counter) {
void void
fps_counter_interrupt(struct fps_counter *counter) { fps_counter_interrupt(struct fps_counter *counter) {
if (!counter->thread) { if (!counter->thread_started) {
return; return;
} }
mutex_lock(counter->mutex); sc_mutex_lock(&counter->mutex);
counter->interrupted = true; counter->interrupted = true;
mutex_unlock(counter->mutex); sc_mutex_unlock(&counter->mutex);
// wake up blocking wait // wake up blocking wait
cond_signal(counter->state_cond); sc_cond_signal(&counter->state_cond);
} }
void void
fps_counter_join(struct fps_counter *counter) { fps_counter_join(struct fps_counter *counter) {
if (counter->thread) { if (counter->thread_started) {
SDL_WaitThread(counter->thread, NULL); sc_thread_join(&counter->thread, NULL);
} }
} }
@ -159,11 +160,11 @@ fps_counter_add_rendered_frame(struct fps_counter *counter) {
return; return;
} }
mutex_lock(counter->mutex); sc_mutex_lock(&counter->mutex);
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now); check_interval_expired(counter, now);
++counter->nr_rendered; ++counter->nr_rendered;
mutex_unlock(counter->mutex); sc_mutex_unlock(&counter->mutex);
} }
void void
@ -172,9 +173,9 @@ fps_counter_add_skipped_frame(struct fps_counter *counter) {
return; return;
} }
mutex_lock(counter->mutex); sc_mutex_lock(&counter->mutex);
uint32_t now = SDL_GetTicks(); uint32_t now = SDL_GetTicks();
check_interval_expired(counter, now); check_interval_expired(counter, now);
++counter->nr_skipped; ++counter->nr_skipped;
mutex_unlock(counter->mutex); sc_mutex_unlock(&counter->mutex);
} }

View File

@ -1,18 +1,20 @@
#ifndef FPSCOUNTER_H #ifndef FPSCOUNTER_H
#define FPSCOUNTER_H #define FPSCOUNTER_H
#include "common.h"
#include <stdatomic.h> #include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include "util/thread.h"
struct fps_counter { struct fps_counter {
SDL_Thread *thread; sc_thread thread;
SDL_mutex *mutex; sc_mutex mutex;
SDL_cond *state_cond; sc_cond state_cond;
bool thread_started;
// atomic so that we can check without locking the mutex // atomic so that we can check without locking the mutex
// if the FPS counter is disabled, we don't want to lock unnecessarily // if the FPS counter is disabled, we don't want to lock unnecessarily

View File

@ -3,9 +3,7 @@
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_keycode.h> #include <SDL2/SDL_keycode.h>
#include "config.h"
#include "event_converter.h" #include "event_converter.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
static const int ACTION_DOWN = 1; static const int ACTION_DOWN = 1;
@ -74,6 +72,10 @@ input_manager_init(struct input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count; im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false; im->vfinger_down = false;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
} }
static void static void
@ -148,13 +150,25 @@ action_cut(struct controller *controller, int actions) {
} }
// turn the screen on if it was off, press BACK otherwise // turn the screen on if it was off, press BACK otherwise
// If the screen is off, it is turned on only on ACTION_DOWN
static void static void
press_back_or_turn_screen_on(struct controller *controller) { press_back_or_turn_screen_on(struct controller *controller, int actions) {
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON; msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
if (actions & ACTION_DOWN) {
msg.back_or_screen_on.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'"); LOGW("Could not request 'press back or turn screen on'");
return;
}
}
if (actions & ACTION_UP) {
msg.back_or_screen_on.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'");
}
} }
} }
@ -169,9 +183,19 @@ expand_notification_panel(struct controller *controller) {
} }
static void static void
collapse_notification_panel(struct controller *controller) { expand_settings_panel(struct controller *controller) {
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL; msg.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'expand settings panel'");
}
}
static void
collapse_panels(struct controller *controller) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'"); LOGW("Could not request 'collapse notification panel'");
@ -191,13 +215,20 @@ set_device_clipboard(struct controller *controller, bool paste) {
return; return;
} }
char *text_dup = strdup(text);
SDL_free(text);
if (!text_dup) {
LOGW("Could not strdup input text");
return;
}
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD; msg.type = CONTROL_MSG_TYPE_SET_CLIPBOARD;
msg.set_clipboard.text = text; msg.set_clipboard.text = text_dup;
msg.set_clipboard.paste = paste; msg.set_clipboard.paste = paste;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
SDL_free(text); free(text_dup);
LOGW("Could not request 'set device clipboard'"); LOGW("Could not request 'set device clipboard'");
} }
} }
@ -243,11 +274,18 @@ clipboard_paste(struct controller *controller) {
return; return;
} }
char *text_dup = strdup(text);
SDL_free(text);
if (!text_dup) {
LOGW("Could not strdup input text");
return;
}
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = text; msg.inject_text.text = text_dup;
if (!controller_push_msg(controller, &msg)) { if (!controller_push_msg(controller, &msg)) {
SDL_free(text); free(text_dup);
LOGW("Could not request 'paste clipboard'"); LOGW("Could not request 'paste clipboard'");
} }
} }
@ -274,7 +312,7 @@ rotate_client_right(struct screen *screen) {
screen_set_rotation(screen, new_rotation); screen_set_rotation(screen, new_rotation);
} }
void static void
input_manager_process_text_input(struct input_manager *im, input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) { const SDL_TextInputEvent *event) {
if (is_shortcut_mod(im, SDL_GetModState())) { if (is_shortcut_mod(im, SDL_GetModState())) {
@ -292,13 +330,13 @@ input_manager_process_text_input(struct input_manager *im,
struct control_msg msg; struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT; msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text); msg.inject_text.text = strdup(event->text);
if (!msg.inject_text.text) { if (!msg.inject_text.text) {
LOGW("Could not strdup input text"); LOGW("Could not strdup input text");
return; return;
} }
if (!controller_push_msg(im->controller, &msg)) { if (!controller_push_msg(im->controller, &msg)) {
SDL_free(msg.inject_text.text); free(msg.inject_text.text);
LOGW("Could not request 'inject text'"); LOGW("Could not request 'inject text'");
} }
} }
@ -354,22 +392,33 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
return true; return true;
} }
void static void
input_manager_process_key(struct input_manager *im, input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event) { const SDL_KeyboardEvent *event) {
// control: indicates the state of the command-line option --no-control // control: indicates the state of the command-line option --no-control
bool control = im->control; bool control = im->control;
bool smod = is_shortcut_mod(im, event->keysym.mod);
struct controller *controller = im->controller; struct controller *controller = im->controller;
SDL_Keycode keycode = event->keysym.sym; SDL_Keycode keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
bool down = event->type == SDL_KEYDOWN; bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL; bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT; bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat; bool repeat = event->repeat;
bool smod = is_shortcut_mod(im, mod);
if (down && !repeat) {
if (keycode == im->last_keycode && mod == im->last_mod) {
++im->key_repeat;
} else {
im->key_repeat = 0;
im->last_keycode = keycode;
im->last_mod = mod;
}
}
// The shortcut modifier is pressed // The shortcut modifier is pressed
if (smod) { if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP; int action = down ? ACTION_DOWN : ACTION_UP;
@ -468,17 +517,17 @@ input_manager_process_key(struct input_manager *im,
return; return;
case SDLK_i: case SDLK_i:
if (!shift && !repeat && down) { if (!shift && !repeat && down) {
struct fps_counter *fps_counter = switch_fps_counter_state(im->fps_counter);
im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
} }
return; return;
case SDLK_n: case SDLK_n:
if (control && !repeat && down) { if (control && !repeat && down) {
if (shift) { if (shift) {
collapse_notification_panel(controller); collapse_panels(controller);
} else { } else if (im->key_repeat == 0) {
expand_notification_panel(controller); expand_notification_panel(controller);
} else {
expand_settings_panel(controller);
} }
} }
return; return;
@ -539,7 +588,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
return true; return true;
} }
void static void
input_manager_process_mouse_motion(struct input_manager *im, input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event) { const SDL_MouseMotionEvent *event) {
if (!event->state) { if (!event->state) {
@ -593,7 +642,7 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
return true; return true;
} }
void static void
input_manager_process_touch(struct input_manager *im, input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) { const SDL_TouchFingerEvent *event) {
struct control_msg msg; struct control_msg msg;
@ -625,7 +674,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
return true; return true;
} }
void static void
input_manager_process_mouse_button(struct input_manager *im, input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event) { const SDL_MouseButtonEvent *event) {
bool control = im->control; bool control = im->control;
@ -636,13 +685,27 @@ input_manager_process_mouse_button(struct input_manager *im,
} }
bool down = event->type == SDL_MOUSEBUTTONDOWN; bool down = event->type == SDL_MOUSEBUTTONDOWN;
if (!im->forward_all_clicks && down) { if (!im->forward_all_clicks) {
int action = down ? ACTION_DOWN : ACTION_UP;
if (control && event->button == SDL_BUTTON_X1) {
action_app_switch(im->controller, action);
return;
}
if (control && event->button == SDL_BUTTON_X2 && down) {
if (event->clicks < 2) {
expand_notification_panel(im->controller);
} else {
expand_settings_panel(im->controller);
}
return;
}
if (control && event->button == SDL_BUTTON_RIGHT) { if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im->controller); press_back_or_turn_screen_on(im->controller, action);
return; return;
} }
if (control && event->button == SDL_BUTTON_MIDDLE) { if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(im->controller, ACTION_DOWN | ACTION_UP); action_home(im->controller, action);
return; return;
} }
@ -655,7 +718,9 @@ input_manager_process_mouse_button(struct input_manager *im,
bool outside = x < r->x || x >= r->x + r->w bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h; || y < r->y || y >= r->y + r->h;
if (outside) { if (outside) {
if (down) {
screen_resize_to_fit(im->screen); screen_resize_to_fit(im->screen);
}
return; return;
} }
} }
@ -724,7 +789,7 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
return true; return true;
} }
void static void
input_manager_process_mouse_wheel(struct input_manager *im, input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) { const SDL_MouseWheelEvent *event) {
struct control_msg msg; struct control_msg msg;
@ -734,3 +799,46 @@ input_manager_process_mouse_wheel(struct input_manager *im,
} }
} }
} }
bool
input_manager_handle_event(struct input_manager *im, SDL_Event *event) {
switch (event->type) {
case SDL_TEXTINPUT:
if (!im->control) {
return true;
}
input_manager_process_text_input(im, &event->text);
return true;
case SDL_KEYDOWN:
case SDL_KEYUP:
// some key events do not interact with the device, so process the
// event even if control is disabled
input_manager_process_key(im, &event->key);
return true;
case SDL_MOUSEMOTION:
if (!im->control) {
break;
}
input_manager_process_mouse_motion(im, &event->motion);
return true;
case SDL_MOUSEWHEEL:
if (!im->control) {
break;
}
input_manager_process_mouse_wheel(im, &event->wheel);
return true;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
input_manager_process_mouse_button(im, &event->button);
return true;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
input_manager_process_touch(im, &event->tfinger);
return true;
}
return false;
}

View File

@ -1,21 +1,20 @@
#ifndef INPUTMANAGER_H #ifndef INPUTMANAGER_H
#define INPUTMANAGER_H #define INPUTMANAGER_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "config.h"
#include "common.h"
#include "controller.h" #include "controller.h"
#include "fps_counter.h" #include "fps_counter.h"
#include "scrcpy.h" #include "scrcpy.h"
#include "screen.h" #include "screen.h"
#include "video_buffer.h"
struct input_manager { struct input_manager {
struct controller *controller; struct controller *controller;
struct video_buffer *video_buffer; struct fps_counter *fps_counter;
struct screen *screen; struct screen *screen;
// SDL reports repeated events as a boolean, but Android expects the actual // SDL reports repeated events as a boolean, but Android expects the actual
@ -34,34 +33,20 @@ struct input_manager {
} sdl_shortcut_mods; } sdl_shortcut_mods;
bool vfinger_down; bool vfinger_down;
// Tracks the number of identical consecutive shortcut key down events.
// Not to be confused with event->repeat, which counts the number of
// system-generated repeated key presses.
unsigned key_repeat;
SDL_Keycode last_keycode;
uint16_t last_mod;
}; };
void void
input_manager_init(struct input_manager *im, input_manager_init(struct input_manager *im,
const struct scrcpy_options *options); const struct scrcpy_options *options);
void bool
input_manager_process_text_input(struct input_manager *im, input_manager_handle_event(struct input_manager *im, SDL_Event *event);
const SDL_TextInputEvent *event);
void
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event);
void
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event);
void
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event);
void
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event);
void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event);
#endif #endif

View File

@ -1,15 +1,18 @@
#include "scrcpy.h" #include "scrcpy.h"
#include "common.h"
#include <assert.h> #include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem #define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "config.h"
#include "cli.h" #include "cli.h"
#include "compat.h"
#include "util/log.h" #include "util/log.h"
static void static void
@ -28,6 +31,11 @@ print_version(void) {
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR, fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR, LIBAVUTIL_VERSION_MINOR,
LIBAVUTIL_VERSION_MICRO); LIBAVUTIL_VERSION_MICRO);
#ifdef HAVE_V4L2
fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR,
LIBAVDEVICE_VERSION_MINOR,
LIBAVDEVICE_VERSION_MICRO);
#endif
} }
static SDL_LogPriority static SDL_LogPriority
@ -90,6 +98,12 @@ main(int argc, char *argv[]) {
av_register_all(); av_register_all();
#endif #endif
#ifdef HAVE_V4L2
if (args.opts.v4l2_device) {
avdevice_register_all();
}
#endif
if (avformat_network_init()) { if (avformat_network_init()) {
return 1; return 1;
} }

View File

@ -1,11 +1,11 @@
#ifndef SC_OPENGL_H #ifndef SC_OPENGL_H
#define SC_OPENGL_H #define SC_OPENGL_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL_opengl.h> #include <SDL2/SDL_opengl.h>
#include "config.h"
struct sc_opengl { struct sc_opengl {
const char *version; const char *version;
bool is_opengles; bool is_opengles;

View File

@ -3,14 +3,13 @@
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_clipboard.h> #include <SDL2/SDL_clipboard.h>
#include "config.h"
#include "device_msg.h" #include "device_msg.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
bool bool
receiver_init(struct receiver *receiver, socket_t control_socket) { receiver_init(struct receiver *receiver, socket_t control_socket) {
if (!(receiver->mutex = SDL_CreateMutex())) { bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
return false; return false;
} }
receiver->control_socket = control_socket; receiver->control_socket = control_socket;
@ -19,7 +18,7 @@ receiver_init(struct receiver *receiver, socket_t control_socket) {
void void
receiver_destroy(struct receiver *receiver) { receiver_destroy(struct receiver *receiver) {
SDL_DestroyMutex(receiver->mutex); sc_mutex_destroy(&receiver->mutex);
} }
static void static void
@ -102,8 +101,9 @@ bool
receiver_start(struct receiver *receiver) { receiver_start(struct receiver *receiver) {
LOGD("Starting receiver thread"); LOGD("Starting receiver thread");
receiver->thread = SDL_CreateThread(run_receiver, "receiver", receiver); bool ok = sc_thread_create(&receiver->thread, run_receiver, "receiver",
if (!receiver->thread) { receiver);
if (!ok) {
LOGC("Could not start receiver thread"); LOGC("Could not start receiver thread");
return false; return false;
} }
@ -113,5 +113,5 @@ receiver_start(struct receiver *receiver) {
void void
receiver_join(struct receiver *receiver) { receiver_join(struct receiver *receiver) {
SDL_WaitThread(receiver->thread, NULL); sc_thread_join(&receiver->thread, NULL);
} }

View File

@ -1,19 +1,19 @@
#ifndef RECEIVER_H #ifndef RECEIVER_H
#define RECEIVER_H #define RECEIVER_H
#include <stdbool.h> #include "common.h"
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h> #include <stdbool.h>
#include "config.h"
#include "util/net.h" #include "util/net.h"
#include "util/thread.h"
// receive events from the device // receive events from the device
// managed by the controller // managed by the controller
struct receiver { struct receiver {
socket_t control_socket; socket_t control_socket;
SDL_Thread *thread; sc_thread thread;
SDL_mutex *mutex; sc_mutex mutex;
}; };
bool bool

View File

@ -3,10 +3,11 @@
#include <assert.h> #include <assert.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include "config.h"
#include "compat.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h"
/** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -22,14 +23,14 @@ find_muxer(const char *name) {
#else #else
oformat = av_oformat_next(oformat); oformat = av_oformat_next(oformat);
#endif #endif
// until null or with name "mp4" // until null or containing the requested name
} while (oformat && strcmp(oformat->name, name)); } while (oformat && !strlist_contains(oformat->name, ',', name));
return oformat; return oformat;
} }
static struct record_packet * static struct record_packet *
record_packet_new(const AVPacket *packet) { record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec)); struct record_packet *rec = malloc(sizeof(*rec));
if (!rec) { if (!rec) {
return NULL; return NULL;
} }
@ -39,7 +40,7 @@ record_packet_new(const AVPacket *packet) {
av_init_packet(&rec->packet); av_init_packet(&rec->packet);
if (av_packet_ref(&rec->packet, packet)) { if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec); free(rec);
return NULL; return NULL;
} }
return rec; return rec;
@ -48,7 +49,7 @@ record_packet_new(const AVPacket *packet) {
static void static void
record_packet_delete(struct record_packet *rec) { record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet); av_packet_unref(&rec->packet);
SDL_free(rec); free(rec);
} }
static void static void
@ -60,50 +61,6 @@ recorder_queue_clear(struct recorder_queue *queue) {
} }
} }
bool
recorder_init(struct recorder *recorder,
const char *filename,
enum sc_record_format format,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
return false;
}
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
recorder->previous = NULL;
return true;
}
void
recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
}
static const char * static const char *
recorder_get_format_name(enum sc_record_format format) { recorder_get_format_name(enum sc_record_format format) {
switch (format) { switch (format) {
@ -113,88 +70,6 @@ recorder_get_format_name(enum sc_record_format format) {
} }
} }
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
const char *format_name = recorder_get_format_name(recorder->format);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
return false;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format;
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
avformat_free_context(recorder->ctx);
return false;
}
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
#else
ostream->codec->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codec->codec_id = input_codec->id;
ostream->codec->pix_fmt = AV_PIX_FMT_YUV420P;
ostream->codec->width = recorder->declared_frame_size.width;
ostream->codec->height = recorder->declared_frame_size.height;
#endif
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
avformat_free_context(recorder->ctx);
return false;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
}
void
recorder_close(struct recorder *recorder) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
}
static bool static bool
recorder_write_header(struct recorder *recorder, const AVPacket *packet) { recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0]; AVStream *ostream = recorder->ctx->streams[0];
@ -208,13 +83,8 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
// copy the first packet to the extra data // copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size); memcpy(extradata, packet->data, packet->size);
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->extradata = extradata; ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size; ostream->codecpar->extradata_size = packet->size;
#else
ostream->codec->extradata = extradata;
ostream->codec->extradata_size = packet->size;
#endif
int ret = avformat_write_header(recorder->ctx, NULL); int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) { if (ret < 0) {
@ -231,7 +101,7 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base); av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
} }
bool static bool
recorder_write(struct recorder *recorder, AVPacket *packet) { recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) { if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) { if (packet->pts != AV_NOPTS_VALUE) {
@ -260,17 +130,17 @@ run_recorder(void *data) {
struct recorder *recorder = data; struct recorder *recorder = data;
for (;;) { for (;;) {
mutex_lock(recorder->mutex); sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && queue_is_empty(&recorder->queue)) { while (!recorder->stopped && queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex); sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
} }
// if stopped is set, continue to process the remaining events (to // if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping // finish the recording) before actually stopping
if (recorder->stopped && queue_is_empty(&recorder->queue)) { if (recorder->stopped && queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
struct record_packet *last = recorder->previous; struct record_packet *last = recorder->previous;
if (last) { if (last) {
// assign an arbitrary duration to the last packet // assign an arbitrary duration to the last packet
@ -290,7 +160,7 @@ run_recorder(void *data) {
struct record_packet *rec; struct record_packet *rec;
queue_take(&recorder->queue, next, &rec); queue_take(&recorder->queue, next, &rec);
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
// recorder->previous is only written from this thread, no need to lock // recorder->previous is only written from this thread, no need to lock
struct record_packet *previous = recorder->previous; struct record_packet *previous = recorder->previous;
@ -313,14 +183,33 @@ run_recorder(void *data) {
if (!ok) { if (!ok) {
LOGE("Could not record packet"); LOGE("Could not record packet");
mutex_lock(recorder->mutex); sc_mutex_lock(&recorder->mutex);
recorder->failed = true; recorder->failed = true;
// discard pending packets // discard pending packets
recorder_queue_clear(&recorder->queue); recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
break; break;
} }
}
if (!recorder->failed) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
}
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
} }
LOGD("Recorder thread ended"); LOGD("Recorder thread ended");
@ -328,53 +217,176 @@ run_recorder(void *data) {
return 0; return 0;
} }
bool static bool
recorder_start(struct recorder *recorder) { recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
LOGD("Starting recorder thread"); bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder); LOGC("Could not create mutex");
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false; return false;
} }
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
LOGC("Could not create cond");
goto error_mutex_destroy;
}
queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->header_written = false;
recorder->previous = NULL;
const char *format_name = recorder_get_format_name(recorder->format);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
goto error_cond_destroy;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOGE("Could not allocate output context");
goto error_cond_destroy;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
recorder->ctx->oformat = (AVOutputFormat *) format;
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
goto error_avformat_free_context;
}
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
goto error_avformat_free_context;
}
LOGD("Starting recorder thread");
ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
recorder);
if (!ok) {
LOGC("Could not start recorder thread");
goto error_avio_close;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true; return true;
error_avio_close:
avio_close(recorder->ctx->pb);
error_avformat_free_context:
avformat_free_context(recorder->ctx);
error_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
return false;
} }
void static void
recorder_stop(struct recorder *recorder) { recorder_close(struct recorder *recorder) {
mutex_lock(recorder->mutex); sc_mutex_lock(&recorder->mutex);
recorder->stopped = true; recorder->stopped = true;
cond_signal(recorder->queue_cond); sc_cond_signal(&recorder->queue_cond);
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
sc_thread_join(&recorder->thread, NULL);
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
} }
void static bool
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) { recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex); sc_mutex_lock(&recorder->mutex);
assert(!recorder->stopped); assert(!recorder->stopped);
if (recorder->failed) { if (recorder->failed) {
// reject any new packet (this will stop the stream) // reject any new packet (this will stop the stream)
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return false; return false;
} }
struct record_packet *rec = record_packet_new(packet); struct record_packet *rec = record_packet_new(packet);
if (!rec) { if (!rec) {
LOGC("Could not allocate record packet"); LOGC("Could not allocate record packet");
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return false; return false;
} }
queue_push(&recorder->queue, next, rec); queue_push(&recorder->queue, next, rec);
cond_signal(recorder->queue_cond); sc_cond_signal(&recorder->queue_cond);
mutex_unlock(recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return true; return true;
} }
static bool
recorder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
struct recorder *recorder = DOWNCAST(sink);
return recorder_open(recorder, codec);
}
static void
recorder_packet_sink_close(struct sc_packet_sink *sink) {
struct recorder *recorder = DOWNCAST(sink);
recorder_close(recorder);
}
static bool
recorder_packet_sink_push(struct sc_packet_sink *sink, const AVPacket *packet) {
struct recorder *recorder = DOWNCAST(sink);
return recorder_push(recorder, packet);
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
enum sc_record_format format,
struct size declared_frame_size) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
return false;
}
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
static const struct sc_packet_sink_ops ops = {
.open = recorder_packet_sink_open,
.close = recorder_packet_sink_close,
.push = recorder_packet_sink_push,
};
recorder->packet_sink.ops = &ops;
return true;
}
void
recorder_destroy(struct recorder *recorder) {
free(recorder->filename);
}

View File

@ -1,15 +1,16 @@
#ifndef RECORDER_H #ifndef RECORDER_H
#define RECORDER_H #define RECORDER_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include "coords.h"
#include "common.h"
#include "scrcpy.h" #include "scrcpy.h"
#include "trait/packet_sink.h"
#include "util/queue.h" #include "util/queue.h"
#include "util/thread.h"
struct record_packet { struct record_packet {
AVPacket packet; AVPacket packet;
@ -19,16 +20,18 @@ struct record_packet {
struct recorder_queue QUEUE(struct record_packet); struct recorder_queue QUEUE(struct record_packet);
struct recorder { struct recorder {
struct sc_packet_sink packet_sink; // packet sink trait
char *filename; char *filename;
enum sc_record_format format; enum sc_record_format format;
AVFormatContext *ctx; AVFormatContext *ctx;
struct size declared_frame_size; struct size declared_frame_size;
bool header_written; bool header_written;
SDL_Thread *thread; sc_thread thread;
SDL_mutex *mutex; sc_mutex mutex;
SDL_cond *queue_cond; sc_cond queue_cond;
bool stopped; // set on recorder_stop() by the stream reader bool stopped; // set on recorder_close()
bool failed; // set on packet write failure bool failed; // set on packet write failure
struct recorder_queue queue; struct recorder_queue queue;
@ -46,22 +49,4 @@ recorder_init(struct recorder *recorder, const char *filename,
void void
recorder_destroy(struct recorder *recorder); recorder_destroy(struct recorder *recorder);
bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec);
void
recorder_close(struct recorder *recorder);
bool
recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
#endif #endif

View File

@ -13,10 +13,6 @@
# include <windows.h> # include <windows.h>
#endif #endif
#include "config.h"
#include "command.h"
#include "common.h"
#include "compat.h"
#include "controller.h" #include "controller.h"
#include "decoder.h" #include "decoder.h"
#include "device.h" #include "device.h"
@ -29,24 +25,27 @@
#include "server.h" #include "server.h"
#include "stream.h" #include "stream.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
static struct server server = SERVER_INITIALIZER; static struct server server;
static struct screen screen = SCREEN_INITIALIZER; static struct screen screen;
static struct fps_counter fps_counter; static struct fps_counter fps_counter;
static struct video_buffer video_buffer;
static struct stream stream; static struct stream stream;
static struct decoder decoder; static struct decoder decoder;
static struct recorder recorder; static struct recorder recorder;
#ifdef HAVE_V4L2
static struct sc_v4l2_sink v4l2_sink;
#endif
static struct controller controller; static struct controller controller;
static struct file_handler file_handler; static struct file_handler file_handler;
static struct input_manager input_manager = { static struct input_manager input_manager = {
.controller = &controller, .controller = &controller,
.video_buffer = &video_buffer, .fps_counter = &fps_counter,
.screen = &screen, .screen = &screen,
.repeat = 0, .repeat = 0,
@ -133,30 +132,6 @@ sdl_init_and_configure(bool display, const char *render_driver,
return true; return true;
} }
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
#ifdef CONTINUOUS_RESIZING_WORKAROUND
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
// not triggered. As a workaround, handle them in an event handler.
//
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
// <https://stackoverflow.com/a/40693139/1987178>
static int
event_watcher(void *data, SDL_Event *event) {
(void) data;
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
screen_render(&screen, true);
}
return 0;
}
#endif
static bool static bool
is_apk(const char *file) { is_apk(const char *file) {
const char *ext = strrchr(file, '.'); const char *ext = strrchr(file, '.');
@ -178,78 +153,42 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
case SDL_QUIT: case SDL_QUIT:
LOGD("User requested to quit"); LOGD("User requested to quit");
return EVENT_RESULT_STOPPED_BY_USER; return EVENT_RESULT_STOPPED_BY_USER;
case EVENT_NEW_FRAME:
if (!screen.has_frame) {
screen.has_frame = true;
// this is the very first frame, show the window
screen_show_window(&screen);
}
if (!screen_update_frame(&screen, &video_buffer)) {
return EVENT_RESULT_CONTINUE;
}
break;
case SDL_WINDOWEVENT:
screen_handle_window_event(&screen, &event->window);
break;
case SDL_TEXTINPUT:
if (!options->control) {
break;
}
input_manager_process_text_input(&input_manager, &event->text);
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
// some key events do not interact with the device, so process the
// event even if control is disabled
input_manager_process_key(&input_manager, &event->key);
break;
case SDL_MOUSEMOTION:
if (!options->control) {
break;
}
input_manager_process_mouse_motion(&input_manager, &event->motion);
break;
case SDL_MOUSEWHEEL:
if (!options->control) {
break;
}
input_manager_process_mouse_wheel(&input_manager, &event->wheel);
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
// some mouse events do not interact with the device, so process
// the event even if control is disabled
input_manager_process_mouse_button(&input_manager, &event->button);
break;
case SDL_FINGERMOTION:
case SDL_FINGERDOWN:
case SDL_FINGERUP:
input_manager_process_touch(&input_manager, &event->tfinger);
break;
case SDL_DROPFILE: { case SDL_DROPFILE: {
if (!options->control) { if (!options->control) {
break; break;
} }
char *file = strdup(event->drop.file);
SDL_free(event->drop.file);
if (!file) {
LOGW("Could not strdup drop filename\n");
break;
}
file_handler_action_t action; file_handler_action_t action;
if (is_apk(event->drop.file)) { if (is_apk(file)) {
action = ACTION_INSTALL_APK; action = ACTION_INSTALL_APK;
} else { } else {
action = ACTION_PUSH_FILE; action = ACTION_PUSH_FILE;
} }
file_handler_request(&file_handler, action, event->drop.file); file_handler_request(&file_handler, action, file);
break; goto end;
} }
} }
bool consumed = screen_handle_event(&screen, event);
if (consumed) {
goto end;
}
consumed = input_manager_handle_event(&input_manager, event);
(void) consumed;
end:
return EVENT_RESULT_CONTINUE; return EVENT_RESULT_CONTINUE;
} }
static bool static bool
event_loop(const struct scrcpy_options *options) { event_loop(const struct scrcpy_options *options) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (options->display) {
SDL_AddEventWatch(event_watcher, NULL);
}
#endif
SDL_Event event; SDL_Event event;
while (SDL_WaitEvent(&event)) { while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, options); enum event_result result = handle_event(&event, options);
@ -290,7 +229,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
if (priority == 0) { if (priority == 0) {
return; return;
} }
char *local_fmt = SDL_malloc(strlen(fmt) + 10); char *local_fmt = malloc(strlen(fmt) + 10);
if (!local_fmt) { if (!local_fmt) {
LOGC("Could not allocate string"); LOGC("Could not allocate string");
return; return;
@ -299,11 +238,29 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
strcpy(local_fmt, "[FFmpeg] "); strcpy(local_fmt, "[FFmpeg] ");
strcpy(local_fmt + 9, fmt); strcpy(local_fmt + 9, fmt);
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl); SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
SDL_free(local_fmt); free(local_fmt);
} }
bool bool
scrcpy(const struct scrcpy_options *options) { scrcpy(const struct scrcpy_options *options) {
if (!server_init(&server)) {
return false;
}
bool ret = false;
bool server_started = false;
bool fps_counter_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
#ifdef HAVE_V4L2
bool v4l2_sink_initialized = false;
#endif
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
bool screen_initialized = false;
bool record = !!options->record_filename; bool record = !!options->record_filename;
struct server_params params = { struct server_params params = {
.log_level = options->log_level, .log_level = options->log_level,
@ -320,20 +277,13 @@ scrcpy(const struct scrcpy_options *options) {
.codec_options = options->codec_options, .codec_options = options->codec_options,
.encoder_name = options->encoder_name, .encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward, .force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
}; };
if (!server_start(&server, options->serial, &params)) { if (!server_start(&server, options->serial, &params)) {
return false; goto end;
} }
bool ret = false; server_started = true;
bool fps_counter_initialized = false;
bool video_buffer_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
if (!sdl_init_and_configure(options->display, options->render_driver, if (!sdl_init_and_configure(options->display, options->render_driver,
options->disable_screensaver)) { options->disable_screensaver)) {
@ -354,19 +304,12 @@ scrcpy(const struct scrcpy_options *options) {
goto end; goto end;
} }
struct decoder *dec = NULL;
if (options->display) { if (options->display) {
if (!fps_counter_init(&fps_counter)) { if (!fps_counter_init(&fps_counter)) {
goto end; goto end;
} }
fps_counter_initialized = true; fps_counter_initialized = true;
if (!video_buffer_init(&video_buffer, &fps_counter,
options->render_expired_frames)) {
goto end;
}
video_buffer_initialized = true;
if (options->control) { if (options->control) {
if (!file_handler_init(&file_handler, server.serial, if (!file_handler_init(&file_handler, server.serial,
options->push_target)) { options->push_target)) {
@ -374,8 +317,15 @@ scrcpy(const struct scrcpy_options *options) {
} }
file_handler_initialized = true; file_handler_initialized = true;
} }
}
decoder_init(&decoder, &video_buffer); struct decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
decoder_init(&decoder);
dec = &decoder; dec = &decoder;
} }
@ -393,14 +343,15 @@ scrcpy(const struct scrcpy_options *options) {
av_log_set_callback(av_log_callback); av_log_set_callback(av_log_callback);
stream_init(&stream, server.video_socket, dec, rec); stream_init(&stream, server.video_socket);
// now we consumed the header values, the socket receives the video stream if (dec) {
// start the stream stream_add_sink(&stream, &dec->packet_sink);
if (!stream_start(&stream)) { }
goto end;
if (rec) {
stream_add_sink(&stream, &rec->packet_sink);
} }
stream_started = true;
if (options->display) { if (options->display) {
if (options->control) { if (options->control) {
@ -418,14 +369,26 @@ scrcpy(const struct scrcpy_options *options) {
const char *window_title = const char *window_title =
options->window_title ? options->window_title : device_name; options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size, struct screen_params screen_params = {
options->always_on_top, options->window_x, .window_title = window_title,
options->window_y, options->window_width, .frame_size = frame_size,
options->window_height, .always_on_top = options->always_on_top,
options->window_borderless, .window_x = options->window_x,
options->rotation, options->mipmaps)) { .window_y = options->window_y,
.window_width = options->window_width,
.window_height = options->window_height,
.window_borderless = options->window_borderless,
.rotation = options->rotation,
.mipmaps = options->mipmaps,
.fullscreen = options->fullscreen,
};
if (!screen_init(&screen, &fps_counter, &screen_params)) {
goto end; goto end;
} }
screen_initialized = true;
decoder_add_sink(&decoder, &screen.frame_sink);
if (options->turn_screen_off) { if (options->turn_screen_off) {
struct control_msg msg; struct control_msg msg;
@ -436,25 +399,39 @@ scrcpy(const struct scrcpy_options *options) {
LOGW("Could not request 'set screen power mode'"); LOGW("Could not request 'set screen power mode'");
} }
} }
}
if (options->fullscreen) { #ifdef HAVE_V4L2
screen_switch_fullscreen(&screen); if (options->v4l2_device) {
if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) {
goto end;
} }
decoder_add_sink(&decoder, &v4l2_sink.frame_sink);
v4l2_sink_initialized = true;
} }
#endif
// now we consumed the header values, the socket receives the video stream
// start the stream
if (!stream_start(&stream)) {
goto end;
}
stream_started = true;
input_manager_init(&input_manager, options); input_manager_init(&input_manager, options);
ret = event_loop(options); ret = event_loop(options);
LOGD("quit..."); LOGD("quit...");
screen_destroy(&screen); // Close the window immediately on closing, because screen_destroy() may
// only be called once the stream thread is joined (it may take time)
screen_hide_window(&screen);
end: end:
// stop stream and controller so that they don't continue once their socket // The stream is not stopped explicitly, because it will stop by itself on
// is shutdown // end-of-stream
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) { if (controller_started) {
controller_stop(&controller); controller_stop(&controller);
} }
@ -465,14 +442,29 @@ end:
fps_counter_interrupt(&fps_counter); fps_counter_interrupt(&fps_counter);
} }
if (server_started) {
// shutdown the sockets and kill the server // shutdown the sockets and kill the server
server_stop(&server); server_stop(&server);
}
// now that the sockets are shutdown, the stream and controller are // now that the sockets are shutdown, the stream and controller are
// interrupted, we can join them // interrupted, we can join them
if (stream_started) { if (stream_started) {
stream_join(&stream); stream_join(&stream);
} }
#ifdef HAVE_V4L2
if (v4l2_sink_initialized) {
sc_v4l2_sink_destroy(&v4l2_sink);
}
#endif
// Destroy the screen only after the stream is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction
if (screen_initialized) {
screen_destroy(&screen);
}
if (controller_started) { if (controller_started) {
controller_join(&controller); controller_join(&controller);
} }
@ -489,10 +481,6 @@ end:
file_handler_destroy(&file_handler); file_handler_destroy(&file_handler);
} }
if (video_buffer_initialized) {
video_buffer_destroy(&video_buffer);
}
if (fps_counter_initialized) { if (fps_counter_initialized) {
fps_counter_join(&fps_counter); fps_counter_join(&fps_counter);
fps_counter_destroy(&fps_counter); fps_counter_destroy(&fps_counter);

View File

@ -1,12 +1,12 @@
#ifndef SCRCPY_H #ifndef SCRCPY_H
#define SCRCPY_H #define SCRCPY_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include "config.h"
enum sc_log_level { enum sc_log_level {
SC_LOG_LEVEL_DEBUG, SC_LOG_LEVEL_DEBUG,
SC_LOG_LEVEL_INFO, SC_LOG_LEVEL_INFO,
@ -20,6 +20,16 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_MKV,
}; };
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
SC_LOCK_VIDEO_ORIENTATION_1,
SC_LOCK_VIDEO_ORIENTATION_2,
SC_LOCK_VIDEO_ORIENTATION_3,
};
#define SC_MAX_SHORTCUT_MODS 8 #define SC_MAX_SHORTCUT_MODS 8
enum sc_shortcut_mod { enum sc_shortcut_mod {
@ -52,6 +62,7 @@ struct scrcpy_options {
const char *render_driver; const char *render_driver;
const char *codec_options; const char *codec_options;
const char *encoder_name; const char *encoder_name;
const char *v4l2_device;
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_record_format record_format; enum sc_record_format record_format;
struct sc_port_range port_range; struct sc_port_range port_range;
@ -59,20 +70,19 @@ struct scrcpy_options {
uint16_t max_size; uint16_t max_size;
uint32_t bit_rate; uint32_t bit_rate;
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; enum sc_lock_video_orientation lock_video_orientation;
uint8_t rotation; uint8_t rotation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint16_t display_id; uint32_t display_id;
bool show_touches; bool show_touches;
bool fullscreen; bool fullscreen;
bool always_on_top; bool always_on_top;
bool control; bool control;
bool display; bool display;
bool turn_screen_off; bool turn_screen_off;
bool render_expired_frames;
bool prefer_text; bool prefer_text;
bool window_borderless; bool window_borderless;
bool mipmaps; bool mipmaps;
@ -82,6 +92,7 @@ struct scrcpy_options {
bool forward_key_repeat; bool forward_key_repeat;
bool forward_all_clicks; bool forward_all_clicks;
bool legacy_paste; bool legacy_paste;
bool power_off_on_close;
}; };
#define SCRCPY_OPTIONS_DEFAULT { \ #define SCRCPY_OPTIONS_DEFAULT { \
@ -93,6 +104,7 @@ struct scrcpy_options {
.render_driver = NULL, \ .render_driver = NULL, \
.codec_options = NULL, \ .codec_options = NULL, \
.encoder_name = NULL, \ .encoder_name = NULL, \
.v4l2_device = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \ .log_level = SC_LOG_LEVEL_INFO, \
.record_format = SC_RECORD_FORMAT_AUTO, \ .record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \ .port_range = { \
@ -103,10 +115,10 @@ struct scrcpy_options {
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \ .data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
.count = 2, \ .count = 2, \
}, \ }, \
.max_size = DEFAULT_MAX_SIZE, \ .max_size = 0, \
.bit_rate = DEFAULT_BIT_RATE, \ .bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \ .max_fps = 0, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \ .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
.rotation = 0, \ .rotation = 0, \
.window_x = SC_WINDOW_POSITION_UNDEFINED, \ .window_x = SC_WINDOW_POSITION_UNDEFINED, \
.window_y = SC_WINDOW_POSITION_UNDEFINED, \ .window_y = SC_WINDOW_POSITION_UNDEFINED, \
@ -119,7 +131,6 @@ struct scrcpy_options {
.control = true, \ .control = true, \
.display = true, \ .display = true, \
.turn_screen_off = false, \ .turn_screen_off = false, \
.render_expired_frames = false, \
.prefer_text = false, \ .prefer_text = false, \
.window_borderless = false, \ .window_borderless = false, \
.mipmaps = true, \ .mipmaps = true, \
@ -129,6 +140,7 @@ struct scrcpy_options {
.forward_key_repeat = true, \ .forward_key_repeat = true, \
.forward_all_clicks = false, \ .forward_all_clicks = false, \
.legacy_paste = false, \ .legacy_paste = false, \
.power_off_on_close = false, \
} }
bool bool

View File

@ -4,18 +4,17 @@
#include <string.h> #include <string.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include "config.h" #include "events.h"
#include "common.h"
#include "compat.h"
#include "icon.xpm" #include "icon.xpm"
#include "scrcpy.h" #include "scrcpy.h"
#include "tiny_xpm.h" #include "tiny_xpm.h"
#include "video_buffer.h" #include "video_buffer.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
#define DISPLAY_MARGINS 96 #define DISPLAY_MARGINS 96
#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink)
static inline struct size static inline struct size
get_rotated_size(struct size size, int rotation) { get_rotated_size(struct size size, int rotation) {
struct size rotated_size; struct size rotated_size;
@ -194,11 +193,6 @@ screen_update_content_rect(struct screen *screen) {
} }
} }
void
screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER;
}
static inline SDL_Texture * static inline SDL_Texture *
create_texture(struct screen *screen) { create_texture(struct screen *screen) {
SDL_Renderer *renderer = screen->renderer; SDL_Renderer *renderer = screen->renderer;
@ -226,27 +220,110 @@ create_texture(struct screen *screen) {
return texture; return texture;
} }
bool #if defined(__APPLE__) || defined(__WINDOWS__)
screen_init_rendering(struct screen *screen, const char *window_title, # define CONTINUOUS_RESIZING_WORKAROUND
struct size frame_size, bool always_on_top, #endif
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless, #ifdef CONTINUOUS_RESIZING_WORKAROUND
uint8_t rotation, bool mipmaps) { // On Windows and MacOS, resizing blocks the event loop, so resizing events are
screen->frame_size = frame_size; // not triggered. As a workaround, handle them in an event handler.
screen->rotation = rotation; //
if (rotation) { // <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
LOGI("Initial display rotation set to %u", rotation); // <https://stackoverflow.com/a/40693139/1987178>
static int
event_watcher(void *data, SDL_Event *event) {
struct screen *screen = data;
if (event->type == SDL_WINDOWEVENT
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
// In practice, it seems to always be called from the same thread in
// that specific case. Anyway, it's just a workaround.
screen_render(screen, true);
} }
struct size content_size = get_rotated_size(frame_size, screen->rotation); return 0;
}
#endif
static bool
screen_frame_sink_open(struct sc_frame_sink *sink) {
struct screen *screen = DOWNCAST(sink);
(void) screen;
#ifndef NDEBUG
screen->open = true;
#endif
// nothing to do, the screen is already open on the main thread
return true;
}
static void
screen_frame_sink_close(struct sc_frame_sink *sink) {
struct screen *screen = DOWNCAST(sink);
(void) screen;
#ifndef NDEBUG
screen->open = false;
#endif
// nothing to do, the screen lifecycle is not managed by the frame producer
}
static bool
screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct screen *screen = DOWNCAST(sink);
bool previous_frame_skipped;
bool ok = video_buffer_push(&screen->vb, frame, &previous_frame_skipped);
if (!ok) {
return false;
}
if (previous_frame_skipped) {
fps_counter_add_skipped_frame(screen->fps_counter);
// The EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead
} else {
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
};
// Post the event on the UI thread
SDL_PushEvent(&new_frame_event);
}
return true;
}
bool
screen_init(struct screen *screen, struct fps_counter *fps_counter,
const struct screen_params *params) {
screen->fps_counter = fps_counter;
screen->resize_pending = false;
screen->has_frame = false;
screen->fullscreen = false;
screen->maximized = false;
bool ok = video_buffer_init(&screen->vb);
if (!ok) {
LOGE("Could not initialize video buffer");
return false;
}
screen->frame_size = params->frame_size;
screen->rotation = params->rotation;
if (screen->rotation) {
LOGI("Initial display rotation set to %u", screen->rotation);
}
struct size content_size =
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size; screen->content_size = content_size;
struct size window_size = struct size window_size = get_initial_optimal_size(content_size,
get_initial_optimal_size(content_size, window_width, window_height); params->window_width,
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; params->window_height);
#ifdef HIDPI_SUPPORT uint32_t window_flags = SDL_WINDOW_HIDDEN
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; | SDL_WINDOW_RESIZABLE
#endif | SDL_WINDOW_ALLOW_HIGHDPI;
if (always_on_top) { if (params->always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP #ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP; window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else #else
@ -254,15 +331,15 @@ screen_init_rendering(struct screen *screen, const char *window_title,
"(compile with SDL >= 2.0.5 to enable it)"); "(compile with SDL >= 2.0.5 to enable it)");
#endif #endif
} }
if (window_borderless) { if (params->window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS; window_flags |= SDL_WINDOW_BORDERLESS;
} }
int x = window_x != SC_WINDOW_POSITION_UNDEFINED int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED; ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != SC_WINDOW_POSITION_UNDEFINED int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED; ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y, screen->window = SDL_CreateWindow(params->window_title, x, y,
window_size.width, window_size.height, window_size.width, window_size.height,
window_flags); window_flags);
if (!screen->window) { if (!screen->window) {
@ -274,7 +351,8 @@ screen_init_rendering(struct screen *screen, const char *window_title,
SDL_RENDERER_ACCELERATED); SDL_RENDERER_ACCELERATED);
if (!screen->renderer) { if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError()); LOGC("Could not create renderer: %s", SDL_GetError());
screen_destroy(screen); SDL_DestroyWindow(screen->window);
video_buffer_destroy(&screen->vb);
return false; return false;
} }
@ -283,15 +361,17 @@ screen_init_rendering(struct screen *screen, const char *window_title,
const char *renderer_name = r ? NULL : renderer_info.name; const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)"); LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
screen->mipmaps = false;
// starts with "opengl" // starts with "opengl"
screen->use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (screen->use_opengl) { if (use_opengl) {
struct sc_opengl *gl = &screen->gl; struct sc_opengl *gl = &screen->gl;
sc_opengl_init(gl); sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version); LOGI("OpenGL version: %s", gl->version);
if (mipmaps) { if (params->mipmaps) {
bool supports_mipmaps = bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */ sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */); 2, 0 /* OpenGL ES 2.0+ */);
@ -305,7 +385,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
} else { } else {
LOGI("Trilinear filtering disabled"); LOGI("Trilinear filtering disabled");
} }
} else { } else if (params->mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)"); LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
} }
@ -317,12 +397,24 @@ screen_init_rendering(struct screen *screen, const char *window_title,
LOGW("Could not load icon"); LOGW("Could not load icon");
} }
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width,
frame_size.height); params->frame_size.height);
screen->texture = create_texture(screen); screen->texture = create_texture(screen);
if (!screen->texture) { if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError()); LOGC("Could not create texture: %s", SDL_GetError());
screen_destroy(screen); SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
video_buffer_destroy(&screen->vb);
return false;
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOGC("Could not create screen frame");
SDL_DestroyTexture(screen->texture);
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
video_buffer_destroy(&screen->vb);
return false; return false;
} }
@ -333,25 +425,49 @@ screen_init_rendering(struct screen *screen, const char *window_title,
screen_update_content_rect(screen); screen_update_content_rect(screen);
if (params->fullscreen) {
screen_switch_fullscreen(screen);
}
#ifdef CONTINUOUS_RESIZING_WORKAROUND
SDL_AddEventWatch(event_watcher, screen);
#endif
static const struct sc_frame_sink_ops ops = {
.open = screen_frame_sink_open,
.close = screen_frame_sink_close,
.push = screen_frame_sink_push,
};
screen->frame_sink.ops = &ops;
#ifndef NDEBUG
screen->open = false;
#endif
return true; return true;
} }
void static void
screen_show_window(struct screen *screen) { screen_show_window(struct screen *screen) {
SDL_ShowWindow(screen->window); SDL_ShowWindow(screen->window);
} }
void
screen_hide_window(struct screen *screen) {
SDL_HideWindow(screen->window);
}
void void
screen_destroy(struct screen *screen) { screen_destroy(struct screen *screen) {
if (screen->texture) { #ifndef NDEBUG
assert(!screen->open);
#endif
av_frame_free(&screen->frame);
SDL_DestroyTexture(screen->texture); SDL_DestroyTexture(screen->texture);
}
if (screen->renderer) {
SDL_DestroyRenderer(screen->renderer); SDL_DestroyRenderer(screen->renderer);
}
if (screen->window) {
SDL_DestroyWindow(screen->window); SDL_DestroyWindow(screen->window);
} video_buffer_destroy(&screen->vb);
} }
static void static void
@ -448,24 +564,25 @@ update_texture(struct screen *screen, const AVFrame *frame) {
frame->data[2], frame->linesize[2]); frame->data[2], frame->linesize[2]);
if (screen->mipmaps) { if (screen->mipmaps) {
assert(screen->use_opengl);
SDL_GL_BindTexture(screen->texture, NULL, NULL); SDL_GL_BindTexture(screen->texture, NULL, NULL);
screen->gl.GenerateMipmap(GL_TEXTURE_2D); screen->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(screen->texture); SDL_GL_UnbindTexture(screen->texture);
} }
} }
bool static bool
screen_update_frame(struct screen *screen, struct video_buffer *vb) { screen_update_frame(struct screen *screen) {
mutex_lock(vb->mutex); av_frame_unref(screen->frame);
const AVFrame *frame = video_buffer_consume_rendered_frame(vb); video_buffer_consume(&screen->vb, screen->frame);
AVFrame *frame = screen->frame;
fps_counter_add_rendered_frame(screen->fps_counter);
struct size new_frame_size = {frame->width, frame->height}; struct size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) { if (!prepare_for_frame(screen, new_frame_size)) {
mutex_unlock(vb->mutex);
return false; return false;
} }
update_texture(screen, frame); update_texture(screen, frame);
mutex_unlock(vb->mutex);
screen_render(screen, false); screen_render(screen, false);
return true; return true;
@ -552,10 +669,26 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
content_size.height); content_size.height);
} }
void bool
screen_handle_window_event(struct screen *screen, screen_handle_event(struct screen *screen, SDL_Event *event) {
const SDL_WindowEvent *event) { switch (event->type) {
switch (event->event) { case EVENT_NEW_FRAME:
if (!screen->has_frame) {
screen->has_frame = true;
// this is the very first frame, show the window
screen_show_window(screen);
}
bool ok = screen_update_frame(screen);
if (!ok) {
LOGW("Frame update failed\n");
}
return true;
case SDL_WINDOWEVENT:
if (!screen->has_frame) {
// Do nothing
return true;
}
switch (event->window.event) {
case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_EXPOSED:
screen_render(screen, true); screen_render(screen, true);
break; break;
@ -567,16 +700,21 @@ screen_handle_window_event(struct screen *screen,
break; break;
case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) { if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling fullscreen // On Windows, in maximized+fullscreen, disabling
// mode unexpectedly triggers the "restored" then "maximized" // fullscreen mode unexpectedly triggers the "restored"
// events, leaving the window in a weird state (maximized // then "maximized" events, leaving the window in a
// according to the events, but not maximized visually). // weird state (maximized according to the events, but
// not maximized visually).
break; break;
} }
screen->maximized = false; screen->maximized = false;
apply_pending_resize(screen); apply_pending_resize(screen);
break; break;
} }
return true;
}
return false;
} }
struct point struct point

View File

@ -1,21 +1,30 @@
#ifndef SCREEN_H #ifndef SCREEN_H
#define SCREEN_H #define SCREEN_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <SDL2/SDL.h> #include <SDL2/SDL.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include "config.h" #include "coords.h"
#include "common.h"
#include "opengl.h" #include "opengl.h"
#include "trait/frame_sink.h"
struct video_buffer; #include "video_buffer.h"
struct screen { struct screen {
struct sc_frame_sink frame_sink; // frame sink trait
#ifndef NDEBUG
bool open; // track the open/close state to assert correct behavior
#endif
struct video_buffer vb;
struct fps_counter *fps_counter;
SDL_Window *window; SDL_Window *window;
SDL_Renderer *renderer; SDL_Renderer *renderer;
SDL_Texture *texture; SDL_Texture *texture;
bool use_opengl;
struct sc_opengl gl; struct sc_opengl gl;
struct size frame_size; struct size frame_size;
struct size content_size; // rotated frame_size struct size content_size; // rotated frame_size
@ -32,67 +41,44 @@ struct screen {
bool has_frame; bool has_frame;
bool fullscreen; bool fullscreen;
bool maximized; bool maximized;
bool no_window;
bool mipmaps; bool mipmaps;
AVFrame *frame;
}; };
#define SCREEN_INITIALIZER { \ struct screen_params {
.window = NULL, \ const char *window_title;
.renderer = NULL, \ struct size frame_size;
.texture = NULL, \ bool always_on_top;
.use_opengl = false, \
.gl = {0}, \
.frame_size = { \
.width = 0, \
.height = 0, \
}, \
.content_size = { \
.width = 0, \
.height = 0, \
}, \
.resize_pending = false, \
.windowed_content_size = { \
.width = 0, \
.height = 0, \
}, \
.rotation = 0, \
.rect = { \
.x = 0, \
.y = 0, \
.w = 0, \
.h = 0, \
}, \
.has_frame = false, \
.fullscreen = false, \
.maximized = false, \
.no_window = false, \
.mipmaps = false, \
}
// initialize default values int16_t window_x;
void int16_t window_y;
screen_init(struct screen *screen); uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED
uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED
bool window_borderless;
uint8_t rotation;
bool mipmaps;
bool fullscreen;
};
// initialize screen, create window, renderer and texture (window is hidden) // initialize screen, create window, renderer and texture (window is hidden)
// window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED
bool bool
screen_init_rendering(struct screen *screen, const char *window_title, screen_init(struct screen *screen, struct fps_counter *fps_counter,
struct size frame_size, bool always_on_top, const struct screen_params *params);
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless,
uint8_t rotation, bool mipmaps);
// show the window
void
screen_show_window(struct screen *screen);
// destroy window, renderer and texture (if any) // destroy window, renderer and texture (if any)
void void
screen_destroy(struct screen *screen); screen_destroy(struct screen *screen);
// resize if necessary and write the rendered frame into the texture // hide the window
bool //
screen_update_frame(struct screen *screen, struct video_buffer *vb); // It is used to hide the window immediately on closing without waiting for
// screen_destroy()
void
screen_hide_window(struct screen *screen);
// render the texture to the renderer // render the texture to the renderer
// //
@ -117,9 +103,9 @@ screen_resize_to_pixel_perfect(struct screen *screen);
void void
screen_set_rotation(struct screen *screen, unsigned rotation); screen_set_rotation(struct screen *screen, unsigned rotation);
// react to window events // react to SDL events
void bool
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event); screen_handle_event(struct screen *screen, SDL_Event *event);
// convert point from window coordinates to frame coordinates // convert point from window coordinates to frame coordinates
// x and y are expressed in pixels // x and y are expressed in pixels

View File

@ -5,12 +5,10 @@
#include <inttypes.h> #include <inttypes.h>
#include <libgen.h> #include <libgen.h>
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_thread.h>
#include <SDL2/SDL_timer.h> #include <SDL2/SDL_timer.h>
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
#include "config.h" #include "adb.h"
#include "command.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/str_util.h" #include "util/str_util.h"
@ -33,7 +31,7 @@ get_server_path(void) {
#ifdef __WINDOWS__ #ifdef __WINDOWS__
char *server_path = utf8_from_wide_char(server_path_env); char *server_path = utf8_from_wide_char(server_path_env);
#else #else
char *server_path = SDL_strdup(server_path_env); char *server_path = strdup(server_path_env);
#endif #endif
if (!server_path) { if (!server_path) {
LOGE("Could not allocate memory"); LOGE("Could not allocate memory");
@ -45,7 +43,7 @@ get_server_path(void) {
#ifndef PORTABLE #ifndef PORTABLE
LOGD("Using server: " DEFAULT_SERVER_PATH); LOGD("Using server: " DEFAULT_SERVER_PATH);
char *server_path = SDL_strdup(DEFAULT_SERVER_PATH); char *server_path = strdup(DEFAULT_SERVER_PATH);
if (!server_path) { if (!server_path) {
LOGE("Could not allocate memory"); LOGE("Could not allocate memory");
return NULL; return NULL;
@ -60,19 +58,19 @@ get_server_path(void) {
LOGE("Could not get executable path, " LOGE("Could not get executable path, "
"using " SERVER_FILENAME " from current directory"); "using " SERVER_FILENAME " from current directory");
// not found, use current directory // not found, use current directory
return SERVER_FILENAME; return strdup(SERVER_FILENAME);
} }
char *dir = dirname(executable_path); char *dir = dirname(executable_path);
size_t dirlen = strlen(dir); size_t dirlen = strlen(dir);
// sizeof(SERVER_FILENAME) gives statically the size including the null byte // sizeof(SERVER_FILENAME) gives statically the size including the null byte
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME); size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len); char *server_path = malloc(len);
if (!server_path) { if (!server_path) {
LOGE("Could not alloc server path string, " LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory"); "using " SERVER_FILENAME " from current directory");
SDL_free(executable_path); free(executable_path);
return SERVER_FILENAME; return strdup(SERVER_FILENAME);
} }
memcpy(server_path, dir, dirlen); memcpy(server_path, dir, dirlen);
@ -80,7 +78,7 @@ get_server_path(void) {
memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME)); memcpy(&server_path[dirlen + 1], SERVER_FILENAME, sizeof(SERVER_FILENAME));
// the final null byte has been copied with SERVER_FILENAME // the final null byte has been copied with SERVER_FILENAME
SDL_free(executable_path); free(executable_path);
LOGD("Using server (portable): %s", server_path); LOGD("Using server (portable): %s", server_path);
return server_path; return server_path;
@ -95,36 +93,36 @@ push_server(const char *serial) {
} }
if (!is_regular_file(server_path)) { if (!is_regular_file(server_path)) {
LOGE("'%s' does not exist or is not a regular file\n", server_path); LOGE("'%s' does not exist or is not a regular file\n", server_path);
SDL_free(server_path); free(server_path);
return false; return false;
} }
process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH); process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH);
SDL_free(server_path); free(server_path);
return process_check_success(process, "adb push"); return process_check_success(process, "adb push", true);
} }
static bool static bool
enable_tunnel_reverse(const char *serial, uint16_t local_port) { enable_tunnel_reverse(const char *serial, uint16_t local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port); process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse"); return process_check_success(process, "adb reverse", true);
} }
static bool static bool
disable_tunnel_reverse(const char *serial) { disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME); process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove"); return process_check_success(process, "adb reverse --remove", true);
} }
static bool static bool
enable_tunnel_forward(const char *serial, uint16_t local_port) { enable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME); process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward"); return process_check_success(process, "adb forward", true);
} }
static bool static bool
disable_tunnel_forward(const char *serial, uint16_t local_port) { disable_tunnel_forward(const char *serial, uint16_t local_port) {
process_t process = adb_forward_remove(serial, local_port); process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove"); return process_check_success(process, "adb forward --remove", true);
} }
static bool static bool
@ -257,12 +255,12 @@ execute_server(struct server *server, const struct server_params *params) {
char bit_rate_string[11]; char bit_rate_string[11];
char max_fps_string[6]; char max_fps_string[6];
char lock_video_orientation_string[5]; char lock_video_orientation_string[5];
char display_id_string[6]; char display_id_string[11];
sprintf(max_size_string, "%"PRIu16, params->max_size); sprintf(max_size_string, "%"PRIu16, params->max_size);
sprintf(bit_rate_string, "%"PRIu32, params->bit_rate); sprintf(bit_rate_string, "%"PRIu32, params->bit_rate);
sprintf(max_fps_string, "%"PRIu16, params->max_fps); sprintf(max_fps_string, "%"PRIu16, params->max_fps);
sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation); sprintf(lock_video_orientation_string, "%"PRIi8, params->lock_video_orientation);
sprintf(display_id_string, "%"PRIu16, params->display_id); sprintf(display_id_string, "%"PRIu32, params->display_id);
const char *const cmd[] = { const char *const cmd[] = {
"shell", "shell",
"CLASSPATH=" DEVICE_SERVER_PATH, "CLASSPATH=" DEVICE_SERVER_PATH,
@ -295,6 +293,7 @@ execute_server(struct server *server, const struct server_params *params) {
params->stay_awake ? "true" : "false", params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-", params->codec_options ? params->codec_options : "-",
params->encoder_name ? params->encoder_name : "-", params->encoder_name ? params->encoder_name : "-",
params->power_off_on_close ? "true" : "false",
}; };
#ifdef SERVER_DEBUGGER #ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port " LOGI("Server debugger waiting for a client on device port "
@ -353,15 +352,48 @@ close_socket(socket_t socket) {
} }
} }
void bool
server_init(struct server *server) { server_init(struct server *server) {
*server = (struct server) SERVER_INITIALIZER; server->serial = NULL;
server->process = PROCESS_NONE;
atomic_flag_clear_explicit(&server->server_socket_closed,
memory_order_relaxed);
bool ok = sc_mutex_init(&server->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&server->process_terminated_cond);
if (!ok) {
sc_mutex_destroy(&server->mutex);
return false;
}
server->process_terminated = false;
server->server_socket = INVALID_SOCKET;
server->video_socket = INVALID_SOCKET;
server->control_socket = INVALID_SOCKET;
server->local_port = 0;
server->tunnel_enabled = false;
server->tunnel_forward = false;
return true;
} }
static int static int
run_wait_server(void *data) { run_wait_server(void *data) {
struct server *server = data; struct server *server = data;
cmd_simple_wait(server->process, NULL); // ignore exit code process_wait(server->process, false); // ignore exit code
sc_mutex_lock(&server->mutex);
server->process_terminated = true;
sc_cond_signal(&server->process_terminated_cond);
sc_mutex_unlock(&server->mutex);
// no need for synchronization, server_socket is initialized before this // no need for synchronization, server_socket is initialized before this
// thread was created // thread was created
if (server->server_socket != INVALID_SOCKET if (server->server_socket != INVALID_SOCKET
@ -377,10 +409,8 @@ run_wait_server(void *data) {
bool bool
server_start(struct server *server, const char *serial, server_start(struct server *server, const char *serial,
const struct server_params *params) { const struct server_params *params) {
server->port_range = params->port_range;
if (serial) { if (serial) {
server->serial = SDL_strdup(serial); server->serial = strdup(serial);
if (!server->serial) { if (!server->serial) {
return false; return false;
} }
@ -407,11 +437,11 @@ server_start(struct server *server, const char *serial,
// things simple and multiplatform, just spawn a new thread waiting for the // things simple and multiplatform, just spawn a new thread waiting for the
// server process and calling shutdown()/close() on the server socket if // server process and calling shutdown()/close() on the server socket if
// necessary to wake up any accept() blocking call. // necessary to wake up any accept() blocking call.
server->wait_server_thread = bool ok = sc_thread_create(&server->wait_server_thread, run_wait_server,
SDL_CreateThread(run_wait_server, "wait-server", server); "wait-server", server);
if (!server->wait_server_thread) { if (!ok) {
cmd_terminate(server->process); process_terminate(server->process);
cmd_simple_wait(server->process, NULL); // ignore exit code process_wait(server->process, true); // ignore exit code
goto error2; goto error2;
} }
@ -430,7 +460,7 @@ error2:
} }
disable_tunnel(server); disable_tunnel(server);
error1: error1:
SDL_free(server->serial); free(server->serial);
return false; return false;
} }
@ -493,17 +523,39 @@ server_stop(struct server *server) {
assert(server->process != PROCESS_NONE); assert(server->process != PROCESS_NONE);
cmd_terminate(server->process);
if (server->tunnel_enabled) { if (server->tunnel_enabled) {
// ignore failure // ignore failure
disable_tunnel(server); disable_tunnel(server);
} }
SDL_WaitThread(server->wait_server_thread, NULL); // Give some delay for the server to terminate properly
sc_mutex_lock(&server->mutex);
bool signaled = false;
if (!server->process_terminated) {
#define WATCHDOG_DELAY_MS 1000
signaled = sc_cond_timedwait(&server->process_terminated_cond,
&server->mutex,
WATCHDOG_DELAY_MS);
}
sc_mutex_unlock(&server->mutex);
// After this delay, kill the server if it's not dead already.
// On some devices, closing the sockets is not sufficient to wake up the
// blocking calls while the device is asleep.
if (!signaled) {
// The process is terminated, but not reaped (closed) yet, so its PID
// is still valid.
LOGW("Killing the server...");
process_terminate(server->process);
}
sc_thread_join(&server->wait_server_thread, NULL);
process_close(server->process);
} }
void void
server_destroy(struct server *server) { server_destroy(struct server *server) {
SDL_free(server->serial); free(server->serial);
sc_cond_destroy(&server->process_terminated_cond);
sc_mutex_destroy(&server->mutex);
} }

View File

@ -1,49 +1,36 @@
#ifndef SERVER_H #ifndef SERVER_H
#define SERVER_H #define SERVER_H
#include "common.h"
#include <stdatomic.h> #include <stdatomic.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include "adb.h"
#include "command.h"
#include "common.h"
#include "scrcpy.h" #include "scrcpy.h"
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/thread.h"
struct server { struct server {
char *serial; char *serial;
process_t process; process_t process;
SDL_Thread *wait_server_thread; sc_thread wait_server_thread;
atomic_flag server_socket_closed; atomic_flag server_socket_closed;
sc_mutex mutex;
sc_cond process_terminated_cond;
bool process_terminated;
socket_t server_socket; // only used if !tunnel_forward socket_t server_socket; // only used if !tunnel_forward
socket_t video_socket; socket_t video_socket;
socket_t control_socket; socket_t control_socket;
struct sc_port_range port_range;
uint16_t local_port; // selected from port_range uint16_t local_port; // selected from port_range
bool tunnel_enabled; bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse" bool tunnel_forward; // use "adb forward" instead of "adb reverse"
}; };
#define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \
.wait_server_thread = NULL, \
.server_socket_closed = ATOMIC_FLAG_INIT, \
.server_socket = INVALID_SOCKET, \
.video_socket = INVALID_SOCKET, \
.control_socket = INVALID_SOCKET, \
.port_range = { \
.first = 0, \
.last = 0, \
}, \
.local_port = 0, \
.tunnel_enabled = false, \
.tunnel_forward = false, \
}
struct server_params { struct server_params {
enum sc_log_level log_level; enum sc_log_level log_level;
const char *crop; const char *crop;
@ -55,14 +42,15 @@ struct server_params {
uint16_t max_fps; uint16_t max_fps;
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint16_t display_id; uint32_t display_id;
bool show_touches; bool show_touches;
bool stay_awake; bool stay_awake;
bool force_adb_forward; bool force_adb_forward;
bool power_off_on_close;
}; };
// init default values // init default values
void bool
server_init(struct server *server); server_init(struct server *server);
// push, enable tunnel et start the server // push, enable tunnel et start the server

View File

@ -4,12 +4,8 @@
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <SDL2/SDL_events.h> #include <SDL2/SDL_events.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include <unistd.h> #include <unistd.h>
#include "config.h"
#include "compat.h"
#include "decoder.h" #include "decoder.h"
#include "events.h" #include "events.h"
#include "recorder.h" #include "recorder.h"
@ -70,25 +66,11 @@ notify_stopped(void) {
} }
static bool static bool
process_config_packet(struct stream *stream, AVPacket *packet) { push_packet_to_sinks(struct stream *stream, const AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) { for (unsigned i = 0; i < stream->sink_count; ++i) {
LOGE("Could not send config packet to recorder"); struct sc_packet_sink *sink = stream->sinks[i];
return false; if (!sink->ops->push(sink, packet)) {
} LOGE("Could not send config packet to sink %d", i);
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false; return false;
} }
} }
@ -115,9 +97,11 @@ stream_parse(struct stream *stream, AVPacket *packet) {
packet->flags |= AV_PKT_FLAG_KEY; packet->flags |= AV_PKT_FLAG_KEY;
} }
bool ok = process_frame(stream, packet); packet->dts = packet->pts;
bool ok = push_packet_to_sinks(stream, packet);
if (!ok) { if (!ok) {
LOGE("Could not process frame"); LOGE("Could not process packet");
return false; return false;
} }
@ -160,7 +144,7 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
if (is_config) { if (is_config) {
// config packet // config packet
bool ok = process_config_packet(stream, packet); bool ok = push_packet_to_sinks(stream, packet);
if (!ok) { if (!ok) {
return false; return false;
} }
@ -181,6 +165,33 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
return true; return true;
} }
static void
stream_close_first_sinks(struct stream *stream, unsigned count) {
while (count) {
struct sc_packet_sink *sink = stream->sinks[--count];
sink->ops->close(sink);
}
}
static inline void
stream_close_sinks(struct stream *stream) {
stream_close_first_sinks(stream, stream->sink_count);
}
static bool
stream_open_sinks(struct stream *stream, const AVCodec *codec) {
for (unsigned i = 0; i < stream->sink_count; ++i) {
struct sc_packet_sink *sink = stream->sinks[i];
if (!sink->ops->open(sink, codec)) {
LOGE("Could not open packet sink %d", i);
stream_close_first_sinks(stream, i);
return false;
}
}
return true;
}
static int static int
run_stream(void *data) { run_stream(void *data) {
struct stream *stream = data; struct stream *stream = data;
@ -197,27 +208,15 @@ run_stream(void *data) {
goto end; goto end;
} }
if (stream->decoder && !decoder_open(stream->decoder, codec)) { if (!stream_open_sinks(stream, codec)) {
LOGE("Could not open decoder"); LOGE("Could not open stream sinks");
goto finally_free_codec_ctx; goto finally_free_codec_ctx;
} }
if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
}
stream->parser = av_parser_init(AV_CODEC_ID_H264); stream->parser = av_parser_init(AV_CODEC_ID_H264);
if (!stream->parser) { if (!stream->parser) {
LOGE("Could not initialize parser"); LOGE("Could not initialize parser");
goto finally_stop_and_join_recorder; goto finally_close_sinks;
} }
// We must only pass complete frames to av_parser_parse2()! // We must only pass complete frames to av_parser_parse2()!
@ -247,20 +246,8 @@ run_stream(void *data) {
} }
av_parser_close(stream->parser); av_parser_close(stream->parser);
finally_stop_and_join_recorder: finally_close_sinks:
if (stream->recorder) { stream_close_sinks(stream);
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_decoder:
if (stream->decoder) {
decoder_close(stream->decoder);
}
finally_free_codec_ctx: finally_free_codec_ctx:
avcodec_free_context(&stream->codec_ctx); avcodec_free_context(&stream->codec_ctx);
end: end:
@ -269,34 +256,33 @@ end:
} }
void void
stream_init(struct stream *stream, socket_t socket, stream_init(struct stream *stream, socket_t socket) {
struct decoder *decoder, struct recorder *recorder) {
stream->socket = socket; stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
stream->has_pending = false; stream->has_pending = false;
stream->sink_count = 0;
}
void
stream_add_sink(struct stream *stream, struct sc_packet_sink *sink) {
assert(stream->sink_count < STREAM_MAX_SINKS);
assert(sink);
assert(sink->ops);
stream->sinks[stream->sink_count++] = sink;
} }
bool bool
stream_start(struct stream *stream) { stream_start(struct stream *stream) {
LOGD("Starting stream thread"); LOGD("Starting stream thread");
stream->thread = SDL_CreateThread(run_stream, "stream", stream); bool ok = sc_thread_create(&stream->thread, run_stream, "stream", stream);
if (!stream->thread) { if (!ok) {
LOGC("Could not start stream thread"); LOGC("Could not start stream thread");
return false; return false;
} }
return true; return true;
} }
void
stream_stop(struct stream *stream) {
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}
}
void void
stream_join(struct stream *stream) { stream_join(struct stream *stream) {
SDL_WaitThread(stream->thread, NULL); sc_thread_join(&stream->thread, NULL);
} }

View File

@ -1,22 +1,26 @@
#ifndef STREAM_H #ifndef STREAM_H
#define STREAM_H #define STREAM_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h> #include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "config.h" #include "trait/packet_sink.h"
#include "util/net.h" #include "util/net.h"
#include "util/thread.h"
struct video_buffer; #define STREAM_MAX_SINKS 2
struct stream { struct stream {
socket_t socket; socket_t socket;
SDL_Thread *thread; sc_thread thread;
struct decoder *decoder;
struct recorder *recorder; struct sc_packet_sink *sinks[STREAM_MAX_SINKS];
unsigned sink_count;
AVCodecContext *codec_ctx; AVCodecContext *codec_ctx;
AVCodecParserContext *parser; AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config // successive packets may need to be concatenated, until a non-config
@ -26,15 +30,14 @@ struct stream {
}; };
void void
stream_init(struct stream *stream, socket_t socket, stream_init(struct stream *stream, socket_t socket);
struct decoder *decoder, struct recorder *recorder);
void
stream_add_sink(struct stream *stream, struct sc_packet_sink *sink);
bool bool
stream_start(struct stream *stream); stream_start(struct stream *stream);
void
stream_stop(struct stream *stream);
void void
stream_join(struct stream *stream); stream_join(struct stream *stream);

View File

@ -1,17 +1,4 @@
// for portability (kill, readlink, strdup, strtok_r) #include "util/process.h"
#define _POSIX_C_SOURCE 200809L
#define _BSD_SOURCE
// modern glibc will complain without this
#define _DEFAULT_SOURCE
#ifdef __APPLE__
# define _DARWIN_C_SOURCE // for strdup(), strtok_r(), memset_pattern4()
#endif
#include "command.h"
#include "config.h"
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
@ -21,14 +8,13 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
#include "util/log.h" #include "util/log.h"
bool bool
cmd_search(const char *file) { search_executable(const char *file) {
char *path = getenv("PATH"); char *path = getenv("PATH");
if (!path) if (!path)
return false; return false;
@ -64,7 +50,7 @@ cmd_search(const char *file) {
} }
enum process_result enum process_result
cmd_execute(const char *const argv[], pid_t *pid) { process_execute(const char *const argv[], pid_t *pid) {
int fd[2]; int fd[2];
if (pipe(fd) == -1) { if (pipe(fd) == -1) {
@ -126,29 +112,37 @@ end:
} }
bool bool
cmd_terminate(pid_t pid) { process_terminate(pid_t pid) {
if (pid <= 0) { if (pid <= 0) {
LOGC("Requested to kill %d, this is an error. Please report the bug.\n", LOGC("Requested to kill %d, this is an error. Please report the bug.\n",
(int) pid); (int) pid);
abort(); abort();
} }
return kill(pid, SIGTERM) != -1; return kill(pid, SIGKILL) != -1;
} }
bool exit_code_t
cmd_simple_wait(pid_t pid, int *exit_code) { process_wait(pid_t pid, bool close) {
int status;
int code; int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { int options = WEXITED;
if (!close) {
options |= WNOWAIT;
}
siginfo_t info;
int r = waitid(P_PID, pid, &info, options);
if (r == -1 || info.si_code != CLD_EXITED) {
// could not wait, or exited unexpectedly, probably by a signal // could not wait, or exited unexpectedly, probably by a signal
code = -1; code = NO_EXIT_CODE;
} else { } else {
code = WEXITSTATUS(status); code = info.si_status;
} }
if (exit_code) { return code;
*exit_code = code; }
}
return !code; void
process_close(pid_t pid) {
process_wait(pid, true); // ignore exit code
} }
char * char *
@ -162,7 +156,7 @@ get_executable_path(void) {
return NULL; return NULL;
} }
buf[len] = '\0'; buf[len] = '\0';
return SDL_strdup(buf); return strdup(buf);
#else #else
// in practice, we only need this feature for portable builds, only used on // in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform // Windows, so we don't care implementing it for every platform

View File

@ -1,8 +1,8 @@
#include "command.h" #include "util/process.h"
#include <assert.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "config.h"
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
@ -21,7 +21,7 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
} }
enum process_result enum process_result
cmd_execute(const char *const argv[], HANDLE *handle) { process_execute(const char *const argv[], HANDLE *handle) {
STARTUPINFOW si; STARTUPINFOW si;
PROCESS_INFORMATION pi; PROCESS_INFORMATION pi;
memset(&si, 0, sizeof(si)); memset(&si, 0, sizeof(si));
@ -39,14 +39,9 @@ cmd_execute(const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
#ifdef WINDOWS_NOCONSOLE if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, 0, NULL, NULL, &si,
int flags = CREATE_NO_WINDOW;
#else
int flags = 0;
#endif
if (!CreateProcessW(NULL, wide, NULL, NULL, FALSE, flags, NULL, NULL, &si,
&pi)) { &pi)) {
SDL_free(wide); free(wide);
*handle = NULL; *handle = NULL;
if (GetLastError() == ERROR_FILE_NOT_FOUND) { if (GetLastError() == ERROR_FILE_NOT_FOUND) {
return PROCESS_ERROR_MISSING_BINARY; return PROCESS_ERROR_MISSING_BINARY;
@ -54,28 +49,35 @@ cmd_execute(const char *const argv[], HANDLE *handle) {
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
SDL_free(wide); free(wide);
*handle = pi.hProcess; *handle = pi.hProcess;
return PROCESS_SUCCESS; return PROCESS_SUCCESS;
} }
bool bool
cmd_terminate(HANDLE handle) { process_terminate(HANDLE handle) {
return TerminateProcess(handle, 1) && CloseHandle(handle); return TerminateProcess(handle, 1);
} }
bool exit_code_t
cmd_simple_wait(HANDLE handle, DWORD *exit_code) { process_wait(HANDLE handle, bool close) {
DWORD code; DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) { || !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code // could not wait or retrieve the exit code
code = -1; // max value, it's unsigned code = NO_EXIT_CODE; // max value, it's unsigned
} }
if (exit_code) { if (close) {
*exit_code = code; CloseHandle(handle);
} }
return !code; return code;
}
void
process_close(HANDLE handle) {
bool closed = CloseHandle(handle);
assert(closed);
(void) closed;
} }
char * char *
@ -103,7 +105,7 @@ is_regular_file(const char *path) {
struct _stat path_stat; struct _stat path_stat;
int r = _wstat(wide_path, &path_stat); int r = _wstat(wide_path, &path_stat);
SDL_free(wide_path); free(wide_path);
if (r) { if (r) {
perror("stat"); perror("stat");

View File

@ -6,7 +6,6 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include "config.h"
#include "util/log.h" #include "util/log.h"
struct index { struct index {

View File

@ -1,9 +1,9 @@
#ifndef TINYXPM_H #ifndef TINYXPM_H
#define TINYXPM_H #define TINYXPM_H
#include <SDL2/SDL.h> #include "common.h"
#include "config.h" #include <SDL2/SDL.h>
SDL_Surface * SDL_Surface *
read_xpm(char *xpm[]); read_xpm(char *xpm[]);

View File

@ -0,0 +1,26 @@
#ifndef SC_FRAME_SINK
#define SC_FRAME_SINK
#include "common.h"
#include <assert.h>
#include <stdbool.h>
typedef struct AVFrame AVFrame;
/**
* Frame sink trait.
*
* Component able to receive AVFrames should implement this trait.
*/
struct sc_frame_sink {
const struct sc_frame_sink_ops *ops;
};
struct sc_frame_sink_ops {
bool (*open)(struct sc_frame_sink *sink);
void (*close)(struct sc_frame_sink *sink);
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
};
#endif

View File

@ -0,0 +1,27 @@
#ifndef SC_PACKET_SINK
#define SC_PACKET_SINK
#include "common.h"
#include <assert.h>
#include <stdbool.h>
typedef struct AVCodec AVCodec;
typedef struct AVPacket AVPacket;
/**
* Packet sink trait.
*
* Component able to receive AVPackets should implement this trait.
*/
struct sc_packet_sink {
const struct sc_packet_sink_ops *ops;
};
struct sc_packet_sink_ops {
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);
};
#endif

View File

@ -1,11 +1,11 @@
#ifndef BUFFER_UTIL_H #ifndef BUFFER_UTIL_H
#define BUFFER_UTIL_H #define BUFFER_UTIL_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include "config.h"
static inline void static inline void
buffer_write16be(uint8_t *buf, uint16_t value) { buffer_write16be(uint8_t *buf, uint16_t value) {
buf[0] = value >> 8; buf[0] = value >> 8;
@ -33,7 +33,7 @@ buffer_read16be(const uint8_t *buf) {
static inline uint32_t static inline uint32_t
buffer_read32be(const uint8_t *buf) { buffer_read32be(const uint8_t *buf) {
return (buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3]; return ((uint32_t) buf[0] << 24) | (buf[1] << 16) | (buf[2] << 8) | buf[3];
} }
static inline uint64_t static inline uint64_t

View File

@ -2,11 +2,11 @@
#ifndef CBUF_H #ifndef CBUF_H
#define CBUF_H #define CBUF_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <unistd.h> #include <unistd.h>
#include "config.h"
// To define a circular buffer type of 20 ints: // To define a circular buffer type of 20 ints:
// struct cbuf_int CBUF(int, 20); // struct cbuf_int CBUF(int, 20);
// //

View File

@ -1,74 +0,0 @@
#ifndef LOCK_H
#define LOCK_H
#include <stdint.h>
#include <SDL2/SDL_mutex.h>
#include "config.h"
#include "log.h"
static inline void
mutex_lock(SDL_mutex *mutex) {
int r = SDL_LockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not lock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
mutex_unlock(SDL_mutex *mutex) {
int r = SDL_UnlockMutex(mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not unlock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline void
cond_wait(SDL_cond *cond, SDL_mutex *mutex) {
int r = SDL_CondWait(cond, mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not wait on condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
static inline int
cond_wait_timeout(SDL_cond *cond, SDL_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond, mutex, ms);
#ifndef NDEBUG
if (r < 0) {
LOGC("Could not wait on condition with timeout: %s", SDL_GetError());
abort();
}
#endif
return r;
}
static inline void
cond_signal(SDL_cond *cond) {
int r = SDL_CondSignal(cond);
#ifndef NDEBUG
if (r) {
LOGC("Could not signal a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
#endif

View File

@ -3,7 +3,6 @@
#include <stdio.h> #include <stdio.h>
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
#include "config.h"
#include "log.h" #include "log.h"
#ifdef __WINDOWS__ #ifdef __WINDOWS__

View File

@ -1,6 +1,8 @@
#ifndef NET_H #ifndef NET_H
#define NET_H #define NET_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_platform.h> #include <SDL2/SDL_platform.h>
@ -17,8 +19,6 @@
typedef int socket_t; typedef int socket_t;
#endif #endif
#include "config.h"
bool bool
net_init(void); net_init(void);

21
app/src/util/process.c Normal file
View File

@ -0,0 +1,21 @@
#include "process.h"
#include "log.h"
bool
process_check_success(process_t proc, const char *name, bool close) {
if (proc == PROCESS_NONE) {
LOGE("Could not execute \"%s\"", name);
return false;
}
exit_code_t exit_code = process_wait(proc, close);
if (exit_code) {
if (exit_code != NO_EXIT_CODE) {
LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code);
} else {
LOGE("\"%s\" exited unexpectedly", name);
}
return false;
}
return true;
}

View File

@ -1,8 +1,9 @@
#ifndef COMMAND_H #ifndef SC_PROCESS_H
#define COMMAND_H #define SC_PROCESS_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <inttypes.h>
#ifdef _WIN32 #ifdef _WIN32
@ -31,58 +32,45 @@
#endif #endif
#include "config.h"
enum process_result { enum process_result {
PROCESS_SUCCESS, PROCESS_SUCCESS,
PROCESS_ERROR_GENERIC, PROCESS_ERROR_GENERIC,
PROCESS_ERROR_MISSING_BINARY, PROCESS_ERROR_MISSING_BINARY,
}; };
#ifndef __WINDOWS__ // execute the command and write the result to the output parameter "process"
bool
cmd_search(const char *file);
#endif
enum process_result enum process_result
cmd_execute(const char *const argv[], process_t *process); process_execute(const char *const argv[], process_t *process);
// kill the process
bool bool
cmd_terminate(process_t pid); process_terminate(process_t pid);
bool // wait and close the process (like waitpid())
cmd_simple_wait(process_t pid, exit_code_t *exit_code); // the "close" flag indicates if the process must be "closed" (reaped)
// (passing false is equivalent to enable WNOWAIT in waitid())
exit_code_t
process_wait(process_t pid, bool close);
process_t // close the process
adb_execute(const char *serial, const char *const adb_cmd[], size_t len); //
// Semantically, process_wait(close) = process_wait(noclose) + process_close
process_t void
adb_forward(const char *serial, uint16_t local_port, process_close(process_t pid);
const char *device_socket_name);
process_t
adb_forward_remove(const char *serial, uint16_t local_port);
process_t
adb_reverse(const char *serial, const char *device_socket_name,
uint16_t local_port);
process_t
adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t
adb_push(const char *serial, const char *local, const char *remote);
process_t
adb_install(const char *serial, const char *local);
// convenience function to wait for a successful process execution // convenience function to wait for a successful process execution
// automatically log process errors with the provided process name // automatically log process errors with the provided process name
bool bool
process_check_success(process_t proc, const char *name); process_check_success(process_t proc, const char *name, bool close);
#ifndef _WIN32
// only used to find package manager, not implemented for Windows
bool
search_executable(const char *file);
#endif
// return the absolute path of the executable (the scrcpy binary) // return the absolute path of the executable (the scrcpy binary)
// may be NULL on error; to be freed by SDL_free // may be NULL on error; to be freed by free()
char * char *
get_executable_path(void); get_executable_path(void);

View File

@ -2,12 +2,12 @@
#ifndef QUEUE_H #ifndef QUEUE_H
#define QUEUE_H #define QUEUE_H
#include "common.h"
#include <assert.h> #include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include "config.h"
// To define a queue type of "struct foo": // To define a queue type of "struct foo":
// struct queue_foo QUEUE(struct foo); // struct queue_foo QUEUE(struct foo);
#define QUEUE(TYPE) { \ #define QUEUE(TYPE) { \

View File

@ -10,10 +10,6 @@
# include <tchar.h> # include <tchar.h>
#endif #endif
#include <SDL2/SDL_stdinc.h>
#include "config.h"
size_t size_t
xstrncpy(char *dest, const char *src, size_t n) { xstrncpy(char *dest, const char *src, size_t n) {
size_t i; size_t i;
@ -51,7 +47,7 @@ truncated:
char * char *
strquote(const char *src) { strquote(const char *src) {
size_t len = strlen(src); size_t len = strlen(src);
char *quoted = SDL_malloc(len + 3); char *quoted = malloc(len + 3);
if (!quoted) { if (!quoted) {
return NULL; return NULL;
} }
@ -144,6 +140,24 @@ parse_integer_with_suffix(const char *s, long *out) {
return true; return true;
} }
bool
strlist_contains(const char *list, char sep, const char *s) {
char *p;
do {
p = strchr(list, sep);
size_t token_len = p ? (size_t) (p - list) : strlen(list);
if (!strncmp(list, s, token_len)) {
return true;
}
if (p) {
list = p + 1;
}
} while (p);
return false;
}
size_t size_t
utf8_truncation_index(const char *utf8, size_t max_len) { utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8); size_t len = strlen(utf8);
@ -169,7 +183,7 @@ utf8_to_wide_char(const char *utf8) {
return NULL; return NULL;
} }
wchar_t *wide = SDL_malloc(len * sizeof(wchar_t)); wchar_t *wide = malloc(len * sizeof(wchar_t));
if (!wide) { if (!wide) {
return NULL; return NULL;
} }
@ -185,7 +199,7 @@ utf8_from_wide_char(const wchar_t *ws) {
return NULL; return NULL;
} }
char *utf8 = SDL_malloc(len); char *utf8 = malloc(len);
if (!utf8) { if (!utf8) {
return NULL; return NULL;
} }

View File

@ -1,11 +1,11 @@
#ifndef STRUTIL_H #ifndef STRUTIL_H
#define STRUTIL_H #define STRUTIL_H
#include "common.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include "config.h"
// like strncpy, except: // like strncpy, except:
// - it copies at most n-1 chars // - it copies at most n-1 chars
// - the dest string is nul-terminated // - the dest string is nul-terminated
@ -43,6 +43,11 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out);
bool bool
parse_integer_with_suffix(const char *s, long *out); parse_integer_with_suffix(const char *s, long *out);
// search s in the list separated by sep
// for example, strlist_contains("a,bc,def", ',', "bc") returns true
bool
strlist_contains(const char *list, char sep, const char *s);
// return the index to truncate a UTF-8 string at a valid position // return the index to truncate a UTF-8 string at a valid position
size_t size_t
utf8_truncation_index(const char *utf8, size_t max_len); utf8_truncation_index(const char *utf8, size_t max_len);

160
app/src/util/thread.c Normal file
View File

@ -0,0 +1,160 @@
#include "thread.h"
#include <assert.h>
#include <SDL2/SDL_thread.h>
#include "log.h"
bool
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
void *userdata) {
SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata);
if (!sdl_thread) {
return false;
}
thread->thread = sdl_thread;
return true;
}
void
sc_thread_join(sc_thread *thread, int *status) {
SDL_WaitThread(thread->thread, status);
}
bool
sc_mutex_init(sc_mutex *mutex) {
SDL_mutex *sdl_mutex = SDL_CreateMutex();
if (!sdl_mutex) {
return false;
}
mutex->mutex = sdl_mutex;
#ifndef NDEBUG
mutex->locker = 0;
#endif
return true;
}
void
sc_mutex_destroy(sc_mutex *mutex) {
SDL_DestroyMutex(mutex->mutex);
}
void
sc_mutex_lock(sc_mutex *mutex) {
// SDL mutexes are recursive, but we don't want to use recursive mutexes
assert(!sc_mutex_held(mutex));
int r = SDL_LockMutex(mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not lock mutex: %s", SDL_GetError());
abort();
}
mutex->locker = sc_thread_get_id();
#else
(void) r;
#endif
}
void
sc_mutex_unlock(sc_mutex *mutex) {
#ifndef NDEBUG
assert(sc_mutex_held(mutex));
mutex->locker = 0;
#endif
int r = SDL_UnlockMutex(mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not lock mutex: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
sc_thread_id
sc_thread_get_id(void) {
return SDL_ThreadID();
}
#ifndef NDEBUG
bool
sc_mutex_held(struct sc_mutex *mutex) {
return mutex->locker == sc_thread_get_id();
}
#endif
bool
sc_cond_init(sc_cond *cond) {
SDL_cond *sdl_cond = SDL_CreateCond();
if (!sdl_cond) {
return false;
}
cond->cond = sdl_cond;
return true;
}
void
sc_cond_destroy(sc_cond *cond) {
SDL_DestroyCond(cond->cond);
}
void
sc_cond_wait(sc_cond *cond, sc_mutex *mutex) {
int r = SDL_CondWait(cond->cond, mutex->mutex);
#ifndef NDEBUG
if (r) {
LOGC("Could not wait on condition: %s", SDL_GetError());
abort();
}
mutex->locker = sc_thread_get_id();
#else
(void) r;
#endif
}
bool
sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms) {
int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms);
#ifndef NDEBUG
if (r < 0) {
LOGC("Could not wait on condition with timeout: %s", SDL_GetError());
abort();
}
mutex->locker = sc_thread_get_id();
#endif
assert(r == 0 || r == SDL_MUTEX_TIMEDOUT);
return r == 0;
}
void
sc_cond_signal(sc_cond *cond) {
int r = SDL_CondSignal(cond->cond);
#ifndef NDEBUG
if (r) {
LOGC("Could not signal a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}
void
sc_cond_broadcast(sc_cond *cond) {
int r = SDL_CondBroadcast(cond->cond);
#ifndef NDEBUG
if (r) {
LOGC("Could not broadcast a condition: %s", SDL_GetError());
abort();
}
#else
(void) r;
#endif
}

81
app/src/util/thread.h Normal file
View File

@ -0,0 +1,81 @@
#ifndef SC_THREAD_H
#define SC_THREAD_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
/* Forward declarations */
typedef struct SDL_Thread SDL_Thread;
typedef struct SDL_mutex SDL_mutex;
typedef struct SDL_cond SDL_cond;
typedef int sc_thread_fn(void *);
typedef unsigned int sc_thread_id;
typedef struct sc_thread {
SDL_Thread *thread;
} sc_thread;
typedef struct sc_mutex {
SDL_mutex *mutex;
#ifndef NDEBUG
sc_thread_id locker;
#endif
} sc_mutex;
typedef struct sc_cond {
SDL_cond *cond;
} sc_cond;
bool
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
void *userdata);
void
sc_thread_join(sc_thread *thread, int *status);
bool
sc_mutex_init(sc_mutex *mutex);
void
sc_mutex_destroy(sc_mutex *mutex);
void
sc_mutex_lock(sc_mutex *mutex);
void
sc_mutex_unlock(sc_mutex *mutex);
sc_thread_id
sc_thread_get_id(void);
#ifndef NDEBUG
bool
sc_mutex_held(struct sc_mutex *mutex);
# define sc_mutex_assert(mutex) assert(sc_mutex_held(mutex))
#else
# define sc_mutex_assert(mutex)
#endif
bool
sc_cond_init(sc_cond *cond);
void
sc_cond_destroy(sc_cond *cond);
void
sc_cond_wait(sc_cond *cond, sc_mutex *mutex);
// return true on signaled, false on timeout
bool
sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, uint32_t ms);
void
sc_cond_signal(sc_cond *cond);
void
sc_cond_broadcast(sc_cond *cond);
#endif

342
app/src/v4l2_sink.c Normal file
View File

@ -0,0 +1,342 @@
#include "v4l2_sink.h"
#include "util/log.h"
#include "util/str_util.h"
/** Downcast frame_sink to sc_v4l2_sink */
#define DOWNCAST(SINK) container_of(SINK, struct sc_v4l2_sink, frame_sink)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
static const AVOutputFormat *
find_muxer(const char *name) {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
void *opaque = NULL;
#endif
const AVOutputFormat *oformat = NULL;
do {
#ifdef SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
oformat = av_muxer_iterate(&opaque);
#else
oformat = av_oformat_next(oformat);
#endif
// until null or containing the requested name
} while (oformat && !strlist_contains(oformat->name, ',', name));
return oformat;
}
static bool
write_header(struct sc_v4l2_sink *vs, const AVPacket *packet) {
AVStream *ostream = vs->format_ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
return false;
}
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
int ret = avformat_write_header(vs->format_ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", vs->device_name);
return false;
}
return true;
}
static void
rescale_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
AVStream *ostream = vs->format_ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
static bool
write_packet(struct sc_v4l2_sink *vs, AVPacket *packet) {
if (!vs->header_written) {
bool ok = write_header(vs, packet);
if (!ok) {
return false;
}
vs->header_written = true;
return true;
}
rescale_packet(vs, packet);
bool ok = av_write_frame(vs->format_ctx, packet) >= 0;
// Failing to write the last frame is not very serious, no future frame may
// depend on it, so the resulting file will still be valid
(void) ok;
return true;
}
static bool
encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
int ret = avcodec_send_frame(vs->encoder_ctx, frame);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Could not send v4l2 video frame: %d", ret);
return false;
}
AVPacket *packet = &vs->packet;
ret = avcodec_receive_packet(vs->encoder_ctx, packet);
if (ret == 0) {
// A packet was received
bool ok = write_packet(vs, packet);
av_packet_unref(packet);
if (!ok) {
LOGW("Could not send packet to v4l2 sink");
return false;
}
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive v4l2 video packet: %d", ret);
return false;
}
return true;
}
static int
run_v4l2_sink(void *data) {
struct sc_v4l2_sink *vs = data;
for (;;) {
sc_mutex_lock(&vs->mutex);
while (!vs->stopped && vs->vb.pending_frame_consumed) {
sc_cond_wait(&vs->cond, &vs->mutex);
}
if (vs->stopped) {
sc_mutex_unlock(&vs->mutex);
break;
}
sc_mutex_unlock(&vs->mutex);
video_buffer_consume(&vs->vb, vs->frame);
bool ok = encode_and_write_frame(vs, vs->frame);
av_frame_unref(vs->frame);
if (!ok) {
LOGE("Could not send frame to v4l2 sink");
break;
}
}
LOGD("V4l2 thread ended");
return 0;
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
bool ok = video_buffer_init(&vs->vb);
if (!ok) {
return false;
}
ok = sc_mutex_init(&vs->mutex);
if (!ok) {
LOGC("Could not create mutex");
goto error_video_buffer_destroy;
}
ok = sc_cond_init(&vs->cond);
if (!ok) {
LOGC("Could not create cond");
goto error_mutex_destroy;
}
// FIXME
const AVOutputFormat *format = find_muxer("video4linux2,v4l2");
if (!format) {
LOGE("Could not find v4l2 muxer");
goto error_cond_destroy;
}
const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO);
if (!encoder) {
LOGE("Raw video encoder not found");
return false;
}
vs->format_ctx = avformat_alloc_context();
if (!vs->format_ctx) {
LOGE("Could not allocate v4l2 output context");
return false;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
// still expects a pointer-to-non-const (it has not be updated accordingly)
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
vs->format_ctx->oformat = (AVOutputFormat *) format;
vs->format_ctx->url = strdup(vs->device_name);
if (!vs->format_ctx->url) {
LOGE("Could not strdup v4l2 device name");
goto error_avformat_free_context;
return false;
}
AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder);
if (!ostream) {
LOGE("Could not allocate new v4l2 stream");
goto error_avformat_free_context;
return false;
}
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) {
LOGE("Failed to open output device: %s", vs->device_name);
// ostream will be cleaned up during context cleaning
goto error_avformat_free_context;
}
vs->encoder_ctx = avcodec_alloc_context3(encoder);
if (!vs->encoder_ctx) {
LOGC("Could not allocate codec context for v4l2");
goto error_avio_close;
}
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;
if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) {
LOGE("Could not open codec for v4l2");
goto error_avcodec_free_context;
}
vs->frame = av_frame_alloc();
if (!vs->frame) {
LOGE("Could not create v4l2 frame");
goto error_avcodec_close;
}
LOGD("Starting v4l2 thread");
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
if (!ok) {
LOGC("Could not start v4l2 thread");
goto error_av_frame_free;
}
vs->header_written = false;
vs->stopped = false;
LOGI("v4l2 sink started to device: %s", vs->device_name);
return true;
error_av_frame_free:
av_frame_free(&vs->frame);
error_avcodec_close:
avcodec_close(vs->encoder_ctx);
error_avcodec_free_context:
avcodec_free_context(&vs->encoder_ctx);
error_avio_close:
avio_close(vs->format_ctx->pb);
error_avformat_free_context:
avformat_free_context(vs->format_ctx);
error_cond_destroy:
sc_cond_destroy(&vs->cond);
error_mutex_destroy:
sc_mutex_destroy(&vs->mutex);
error_video_buffer_destroy:
video_buffer_destroy(&vs->vb);
return false;
}
static void
sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
sc_mutex_lock(&vs->mutex);
vs->stopped = true;
sc_cond_signal(&vs->cond);
sc_mutex_unlock(&vs->mutex);
sc_thread_join(&vs->thread, NULL);
av_frame_free(&vs->frame);
avcodec_close(vs->encoder_ctx);
avcodec_free_context(&vs->encoder_ctx);
avio_close(vs->format_ctx->pb);
avformat_free_context(vs->format_ctx);
sc_cond_destroy(&vs->cond);
sc_mutex_destroy(&vs->mutex);
video_buffer_destroy(&vs->vb);
}
static bool
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
bool ok = video_buffer_push(&vs->vb, frame, NULL);
if (!ok) {
return false;
}
// signal possible change of vs->vb.pending_frame_consumed
sc_cond_signal(&vs->cond);
return true;
}
static bool
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs);
}
static void
sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
sc_v4l2_sink_close(vs);
}
static bool
sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_push(vs, frame);
}
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct 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,
.push = sc_v4l2_frame_sink_push,
};
vs->frame_sink.ops = &ops;
return true;
}
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) {
free(vs->device_name);
}

39
app/src/v4l2_sink.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef SC_V4L2_SINK_H
#define SC_V4L2_SINK_H
#include "common.h"
#include "coords.h"
#include "trait/frame_sink.h"
#include "video_buffer.h"
#include <libavformat/avformat.h>
struct sc_v4l2_sink {
struct sc_frame_sink frame_sink; // frame sink trait
struct video_buffer vb;
AVFormatContext *format_ctx;
AVCodecContext *encoder_ctx;
char *device_name;
struct size frame_size;
sc_thread thread;
sc_mutex mutex;
sc_cond cond;
bool stopped;
bool header_written;
AVFrame *frame;
AVPacket packet;
};
bool
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
struct size frame_size);
void
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
#endif

View File

@ -1,113 +1,88 @@
#include "video_buffer.h" #include "video_buffer.h"
#include <assert.h> #include <assert.h>
#include <SDL2/SDL_mutex.h>
#include <libavutil/avutil.h> #include <libavutil/avutil.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include "config.h"
#include "util/lock.h"
#include "util/log.h" #include "util/log.h"
bool bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, video_buffer_init(struct video_buffer *vb) {
bool render_expired_frames) { vb->pending_frame = av_frame_alloc();
vb->fps_counter = fps_counter; if (!vb->pending_frame) {
return false;
if (!(vb->decoding_frame = av_frame_alloc())) {
goto error_0;
} }
if (!(vb->rendering_frame = av_frame_alloc())) { vb->tmp_frame = av_frame_alloc();
goto error_1; if (!vb->tmp_frame) {
av_frame_free(&vb->pending_frame);
return false;
} }
if (!(vb->mutex = SDL_CreateMutex())) { bool ok = sc_mutex_init(&vb->mutex);
goto error_2; if (!ok) {
av_frame_free(&vb->pending_frame);
av_frame_free(&vb->tmp_frame);
return false;
} }
vb->render_expired_frames = render_expired_frames; // there is initially no frame, so consider it has already been consumed
if (render_expired_frames) { vb->pending_frame_consumed = true;
if (!(vb->rendering_frame_consumed_cond = SDL_CreateCond())) {
SDL_DestroyMutex(vb->mutex);
goto error_2;
}
// interrupted is not used if expired frames are not rendered
// since offering a frame will never block
vb->interrupted = false;
}
// there is initially no rendering frame, so consider it has already been
// consumed
vb->rendering_frame_consumed = true;
return true; return true;
error_2:
av_frame_free(&vb->rendering_frame);
error_1:
av_frame_free(&vb->decoding_frame);
error_0:
return false;
} }
void void
video_buffer_destroy(struct video_buffer *vb) { video_buffer_destroy(struct video_buffer *vb) {
if (vb->render_expired_frames) { sc_mutex_destroy(&vb->mutex);
SDL_DestroyCond(vb->rendering_frame_consumed_cond); av_frame_free(&vb->pending_frame);
} av_frame_free(&vb->tmp_frame);
SDL_DestroyMutex(vb->mutex);
av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->decoding_frame);
} }
static void static inline void
video_buffer_swap_frames(struct video_buffer *vb) { swap_frames(AVFrame **lhs, AVFrame **rhs) {
AVFrame *tmp = vb->decoding_frame; AVFrame *tmp = *lhs;
vb->decoding_frame = vb->rendering_frame; *lhs = *rhs;
vb->rendering_frame = tmp; *rhs = tmp;
} }
void bool
video_buffer_offer_decoded_frame(struct video_buffer *vb, video_buffer_push(struct video_buffer *vb, const AVFrame *frame,
bool *previous_frame_skipped) { bool *previous_frame_skipped) {
mutex_lock(vb->mutex); sc_mutex_lock(&vb->mutex);
if (vb->render_expired_frames) {
// wait for the current (expired) frame to be consumed // Use a temporary frame to preserve pending_frame in case of error.
while (!vb->rendering_frame_consumed && !vb->interrupted) { // tmp_frame is an empty frame, no need to call av_frame_unref() beforehand.
cond_wait(vb->rendering_frame_consumed_cond, vb->mutex); int r = av_frame_ref(vb->tmp_frame, frame);
} if (r) {
} else if (!vb->rendering_frame_consumed) { LOGE("Could not ref frame: %d", r);
fps_counter_add_skipped_frame(vb->fps_counter); return false;
} }
video_buffer_swap_frames(vb); // Now that av_frame_ref() succeeded, we can replace the previous
// pending_frame
swap_frames(&vb->pending_frame, &vb->tmp_frame);
av_frame_unref(vb->tmp_frame);
*previous_frame_skipped = !vb->rendering_frame_consumed; if (previous_frame_skipped) {
vb->rendering_frame_consumed = false; *previous_frame_skipped = !vb->pending_frame_consumed;
mutex_unlock(vb->mutex);
}
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb) {
assert(!vb->rendering_frame_consumed);
vb->rendering_frame_consumed = true;
fps_counter_add_rendered_frame(vb->fps_counter);
if (vb->render_expired_frames) {
// unblock video_buffer_offer_decoded_frame()
cond_signal(vb->rendering_frame_consumed_cond);
} }
return vb->rendering_frame; vb->pending_frame_consumed = false;
sc_mutex_unlock(&vb->mutex);
return true;
} }
void void
video_buffer_interrupt(struct video_buffer *vb) { video_buffer_consume(struct video_buffer *vb, AVFrame *dst) {
if (vb->render_expired_frames) { sc_mutex_lock(&vb->mutex);
mutex_lock(vb->mutex); assert(!vb->pending_frame_consumed);
vb->interrupted = true; vb->pending_frame_consumed = true;
mutex_unlock(vb->mutex);
// wake up blocking wait av_frame_move_ref(dst, vb->pending_frame);
cond_signal(vb->rendering_frame_consumed_cond); // av_frame_move_ref() resets its source frame, so no need to call
} // av_frame_unref()
sc_mutex_unlock(&vb->mutex);
} }

View File

@ -1,49 +1,50 @@
#ifndef VIDEO_BUFFER_H #ifndef VIDEO_BUFFER_H
#define VIDEO_BUFFER_H #define VIDEO_BUFFER_H
#include <stdbool.h> #include "common.h"
#include <SDL2/SDL_mutex.h>
#include <stdbool.h>
#include "config.h"
#include "fps_counter.h" #include "fps_counter.h"
#include "util/thread.h"
// forward declarations // forward declarations
typedef struct AVFrame AVFrame; typedef struct AVFrame AVFrame;
/**
* A video buffer holds 1 pending frame, which is the last frame received from
* the producer (typically, the decoder).
*
* If a pending frame has not been consumed when the producer pushes a new
* frame, then it is lost. The intent is to always provide access to the very
* last frame to minimize latency.
*
* The producer and the consumer typically do not live in the same thread.
* That's the reason why the callback on_frame_available() does not provide the
* frame as parameter: the consumer might post an event to its own thread to
* retrieve the pending frame from there, and that frame may have changed since
* the callback if producer pushed a new one in between.
*/
struct video_buffer { struct video_buffer {
AVFrame *decoding_frame; AVFrame *pending_frame;
AVFrame *rendering_frame; AVFrame *tmp_frame; // To preserve the pending frame on error
SDL_mutex *mutex;
bool render_expired_frames; sc_mutex mutex;
bool interrupted;
SDL_cond *rendering_frame_consumed_cond; bool pending_frame_consumed;
bool rendering_frame_consumed;
struct fps_counter *fps_counter;
}; };
bool bool
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter, video_buffer_init(struct video_buffer *vb);
bool render_expired_frames);
void void
video_buffer_destroy(struct video_buffer *vb); video_buffer_destroy(struct video_buffer *vb);
// set the decoded frame as ready for rendering bool
// this function locks frames->mutex during its execution video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped);
// the output flag is set to report whether the previous frame has been skipped
void
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped);
// mark the rendering frame as consumed and return it
// MUST be called with frames->mutex locked!!!
// the caller is expected to render the returned frame to some texture before
// unlocking frames->mutex
const AVFrame *
video_buffer_consume_rendered_frame(struct video_buffer *vb);
// wake up and avoid any blocking call
void void
video_buffer_interrupt(struct video_buffer *vb); video_buffer_consume(struct video_buffer *vb, AVFrame *dst);
#endif #endif

View File

@ -1,3 +1,5 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include "util/buffer_util.h" #include "util/buffer_util.h"

View File

@ -1,3 +1,5 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>

View File

@ -1,8 +1,9 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include "cli.h" #include "cli.h"
#include "common.h"
#include "scrcpy.h" #include "scrcpy.h"
static void test_flag_version(void) { static void test_flag_version(void) {
@ -57,7 +58,6 @@ static void test_options(void) {
"--push-target", "/sdcard/Movies", "--push-target", "/sdcard/Movies",
"--record", "file", "--record", "file",
"--record-format", "mkv", "--record-format", "mkv",
"--render-expired-frames",
"--serial", "0123456789abcdef", "--serial", "0123456789abcdef",
"--show-touches", "--show-touches",
"--turn-screen-off", "--turn-screen-off",
@ -86,7 +86,6 @@ static void test_options(void) {
assert(!strcmp(opts->push_target, "/sdcard/Movies")); assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file")); assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == SC_RECORD_FORMAT_MKV); assert(opts->record_format == SC_RECORD_FORMAT_MKV);
assert(opts->render_expired_frames);
assert(!strcmp(opts->serial, "0123456789abcdef")); assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches); assert(opts->show_touches);
assert(opts->turn_screen_off); assert(opts->turn_screen_off);

View File

@ -1,3 +1,5 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
@ -15,7 +17,7 @@ static void test_serialize_inject_keycode(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 14); assert(size == 14);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -37,7 +39,7 @@ static void test_serialize_inject_text(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 18); assert(size == 18);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -57,7 +59,7 @@ static void test_serialize_inject_text_long(void) {
msg.inject_text.text = text; msg.inject_text.text = text;
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH); assert(size == 5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH]; unsigned char expected[5 + CONTROL_MSG_INJECT_TEXT_MAX_LENGTH];
@ -93,7 +95,7 @@ static void test_serialize_inject_touch_event(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 28); assert(size == 28);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -128,7 +130,7 @@ static void test_serialize_inject_scroll_event(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 21); assert(size == 21);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -144,14 +146,18 @@ static void test_serialize_inject_scroll_event(void) {
static void test_serialize_back_or_screen_on(void) { static void test_serialize_back_or_screen_on(void) {
struct control_msg msg = { struct control_msg msg = {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, .type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
.back_or_screen_on = {
.action = AKEY_EVENT_ACTION_UP,
},
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 2);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON, CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
0x01, // AKEY_EVENT_ACTION_UP
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -162,7 +168,7 @@ static void test_serialize_expand_notification_panel(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -171,17 +177,32 @@ static void test_serialize_expand_notification_panel(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_collapse_notification_panel(void) { static void test_serialize_expand_settings_panel(void) {
struct control_msg msg = { struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, .type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL, CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_collapse_panels(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_COLLAPSE_PANELS,
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
@ -192,7 +213,7 @@ static void test_serialize_get_clipboard(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -211,7 +232,7 @@ static void test_serialize_set_clipboard(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 19); assert(size == 19);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -232,7 +253,7 @@ static void test_serialize_set_screen_power_mode(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 2); assert(size == 2);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -248,7 +269,7 @@ static void test_serialize_rotate_device(void) {
}; };
unsigned char buf[CONTROL_MSG_MAX_SIZE]; unsigned char buf[CONTROL_MSG_MAX_SIZE];
int size = control_msg_serialize(&msg, buf); size_t size = control_msg_serialize(&msg, buf);
assert(size == 1); assert(size == 1);
const unsigned char expected[] = { const unsigned char expected[] = {
@ -268,7 +289,8 @@ int main(int argc, char *argv[]) {
test_serialize_inject_scroll_event(); test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on(); test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel(); test_serialize_expand_notification_panel();
test_serialize_collapse_notification_panel(); test_serialize_expand_settings_panel();
test_serialize_collapse_panels();
test_serialize_get_clipboard(); test_serialize_get_clipboard();
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_screen_power_mode(); test_serialize_set_screen_power_mode();

View File

@ -1,3 +1,5 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>

View File

@ -1,3 +1,5 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include "util/queue.h" #include "util/queue.h"

View File

@ -1,8 +1,9 @@
#include "common.h"
#include <assert.h> #include <assert.h>
#include <limits.h> #include <limits.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <SDL2/SDL.h>
#include "util/str_util.h" #include "util/str_util.h"
@ -136,7 +137,7 @@ static void test_strquote(void) {
// add '"' at the beginning and the end // add '"' at the beginning and the end
assert(!strcmp("\"abcde\"", out)); assert(!strcmp("\"abcde\"", out));
SDL_free(out); free(out);
} }
static void test_utf8_truncate(void) { static void test_utf8_truncate(void) {
@ -286,6 +287,18 @@ static void test_parse_integer_with_suffix(void) {
assert(!ok); assert(!ok);
} }
static void test_strlist_contains(void) {
assert(strlist_contains("a,bc,def", ',', "bc"));
assert(!strlist_contains("a,bc,def", ',', "b"));
assert(strlist_contains("", ',', ""));
assert(strlist_contains("abc,", ',', ""));
assert(strlist_contains(",abc", ',', ""));
assert(strlist_contains("abc,,def", ',', ""));
assert(!strlist_contains("abc", ',', ""));
assert(strlist_contains(",,|x", '|', ",,"));
assert(strlist_contains("xyz", '\0', "xyz"));
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@ -303,5 +316,6 @@ int main(int argc, char *argv[]) {
test_parse_integer(); test_parse_integer();
test_parse_integers(); test_parse_integers();
test_parse_integer_with_suffix(); test_parse_integer_with_suffix();
test_strlist_contains();
return 0; return 0;
} }

View File

@ -2,11 +2,11 @@
[binaries] [binaries]
name = 'mingw' name = 'mingw'
c = '/usr/bin/i686-w64-mingw32-gcc' c = 'i686-w64-mingw32-gcc'
cpp = '/usr/bin/i686-w64-mingw32-g++' cpp = 'i686-w64-mingw32-g++'
ar = '/usr/bin/i686-w64-mingw32-ar' ar = 'i686-w64-mingw32-ar'
strip = '/usr/bin/i686-w64-mingw32-strip' strip = 'i686-w64-mingw32-strip'
pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config' pkgconfig = 'i686-w64-mingw32-pkg-config'
[host_machine] [host_machine]
system = 'windows' system = 'windows'
@ -17,4 +17,4 @@ endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/i686-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.14/i686-w64-mingw32'

View File

@ -2,11 +2,11 @@
[binaries] [binaries]
name = 'mingw' name = 'mingw'
c = '/usr/bin/x86_64-w64-mingw32-gcc' c = 'x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-g++' cpp = 'x86_64-w64-mingw32-g++'
ar = '/usr/bin/x86_64-w64-mingw32-ar' ar = 'x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip' strip = 'x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config' pkgconfig = 'x86_64-w64-mingw32-pkg-config'
[host_machine] [host_machine]
system = 'windows' system = 'windows'
@ -17,4 +17,4 @@ endian = 'little'
[properties] [properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared' prebuilt_ffmpeg_shared = 'ffmpeg-4.3.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev' prebuilt_ffmpeg_dev = 'ffmpeg-4.3.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.12/x86_64-w64-mingw32' prebuilt_sdl2 = 'SDL2-2.0.14/x86_64-w64-mingw32'

View File

@ -0,0 +1,7 @@
strCommand = "cmd /c scrcpy.exe"
For Each Arg In WScript.Arguments
strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """"
Next
CreateObject("Wscript.Shell").Run strCommand, 0, false

View File

@ -1,9 +1,10 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '1.16', version: '1.17',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',
'warning_level=2', 'warning_level=2',
'b_ndebug=if-release',
]) ])
if get_option('compile_app') if get_option('compile_app')

View File

@ -1,9 +1,7 @@
option('compile_app', type: 'boolean', value: true, description: 'Build the client') option('compile_app', type: 'boolean', value: true, description: 'Build the client')
option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux') option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')
option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached')
option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')

View File

@ -30,9 +30,9 @@ prepare-ffmpeg-dev-win64:
ffmpeg-4.3.1-win64-dev ffmpeg-4.3.1-win64-dev
prepare-sdl2: prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.12-mingw.tar.gz \ @./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.14-mingw.tar.gz \
e614a60f797e35ef9f3f96aef3dc6a1d786de3cc7ca6216f97e435c0b6aafc46 \ 405eaff3eb18f2e08fe669ef9e63bc9a8710b7d343756f238619761e9b60407d \
SDL2-2.0.12 SDL2-2.0.14
prepare-adb: prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \ @./prepare-dep https://dl.google.com/android/repository/platform-tools_r30.0.5-windows.zip \

View File

@ -9,21 +9,20 @@
# the server to the device. # the server to the device.
.PHONY: default clean \ .PHONY: default clean \
test \
build-server \ build-server \
prepare-deps-win32 prepare-deps-win64 \ prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win32-noconsole \ build-win32 build-win64 \
build-win64 build-win64-noconsole \
dist-win32 dist-win64 \ dist-win32 dist-win64 \
zip-win32 zip-win64 \ zip-win32 zip-win64 \
sums release release
GRADLE ?= ./gradlew GRADLE ?= ./gradlew
TEST_BUILD_DIR := build-test
SERVER_BUILD_DIR := build-server SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32 WIN32_BUILD_DIR := build-win32
WIN32_NOCONSOLE_BUILD_DIR := build-win32-noconsole
WIN64_BUILD_DIR := build-win64 WIN64_BUILD_DIR := build-win64
WIN64_NOCONSOLE_BUILD_DIR := build-win64-noconsole
DIST := dist DIST := dist
WIN32_TARGET_DIR := scrcpy-win32 WIN32_TARGET_DIR := scrcpy-win32
@ -33,18 +32,34 @@ VERSION := $(shell git describe --tags --always)
WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip WIN32_TARGET := $(WIN32_TARGET_DIR)-$(VERSION).zip
WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip WIN64_TARGET := $(WIN64_TARGET_DIR)-$(VERSION).zip
release: clean zip-win32 zip-win64 sums RELEASE_DIR := release-$(VERSION)
@echo "Windows archives generated in $(DIST)/"
release: clean test build-server zip-win32 zip-win64
mkdir -p "$(RELEASE_DIR)"
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
cd "$(RELEASE_DIR)" && \
sha256sum "scrcpy-server-$(VERSION)" \
"scrcpy-win32-$(VERSION).zip" \
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
@echo "Release generated in $(RELEASE_DIR)/"
clean: clean:
$(GRADLE) clean $(GRADLE) clean
rm -rf "$(SERVER_BUILD_DIR)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" \ rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
"$(WIN32_NOCONSOLE_BUILD_DIR)" "$(WIN64_NOCONSOLE_BUILD_DIR)" "$(DIST)" "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
test:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
$(GRADLE) -p server check
build-server: build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \ meson "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
--buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)" ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32: prepare-deps-win32:
@ -60,17 +75,6 @@ build-win32: prepare-deps-win32
-Dportable=true ) -Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
build-win32-noconsole: prepare-deps-win32
[ -d "$(WIN32_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN32_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN32_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
prepare-deps-win64: prepare-deps-win64:
-$(MAKE) -C prebuilt-deps prepare-win64 -$(MAKE) -C prebuilt-deps prepare-win64
@ -84,23 +88,12 @@ build-win64: prepare-deps-win64
-Dportable=true ) -Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)" ninja -C "$(WIN64_BUILD_DIR)"
build-win64-noconsole: prepare-deps-win64 dist-win32: build-server build-win32
[ -d "$(WIN64_NOCONSOLE_BUILD_DIR)" ] || ( mkdir "$(WIN64_NOCONSOLE_BUILD_DIR)" && \
meson "$(WIN64_NOCONSOLE_BUILD_DIR)" \
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
dist-win32: build-server build-win32 build-win32-noconsole
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@ -109,14 +102,14 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.14/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/ffmpeg-4.3.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
@ -125,7 +118,7 @@ dist-win64: build-server build-win64 build-win64-noconsole
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.12/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp prebuilt-deps/SDL2-2.0.14/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)/$(WIN32_TARGET_DIR)"; \ cd "$(DIST)/$(WIN32_TARGET_DIR)"; \
@ -134,7 +127,3 @@ zip-win32: dist-win32
zip-win64: dist-win64 zip-win64: dist-win64
cd "$(DIST)/$(WIN64_TARGET_DIR)"; \ cd "$(DIST)/$(WIN64_TARGET_DIR)"; \
zip -r "../$(WIN64_TARGET)" . zip -r "../$(WIN64_TARGET)" .
sums:
cd "$(DIST)"; \
sha256sum *.zip > SHA256SUMS.txt

View File

@ -1,44 +1,2 @@
#!/bin/bash #!/bin/bash
set -e make -f release.mk
# test locally
TESTDIR=build_test
rm -rf "$TESTDIR"
# run client tests with ASAN enabled
meson "$TESTDIR" -Db_sanitize=address
ninja -C"$TESTDIR" test
# test server
GRADLE=${GRADLE:-./gradlew}
$GRADLE -p server check
BUILDDIR=build_release
rm -rf "$BUILDDIR"
meson "$BUILDDIR" --buildtype release --strip -Db_lto=true
cd "$BUILDDIR"
ninja
cd -
# build Windows releases
make -f Makefile.CrossWindows
# the generated server must be the same everywhere
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win32/scrcpy-server
cmp "$BUILDDIR/server/scrcpy-server" dist/scrcpy-win64/scrcpy-server
# get version name
TAG=$(git describe --tags --always)
# create release directory
mkdir -p "release-$TAG"
cp "$BUILDDIR/server/scrcpy-server" "release-$TAG/scrcpy-server-$TAG"
cp "dist/scrcpy-win32-$TAG.zip" "release-$TAG/"
cp "dist/scrcpy-win64-$TAG.zip" "release-$TAG/"
# generate checksums
cd "release-$TAG"
sha256sum "scrcpy-server-$TAG" \
"scrcpy-win32-$TAG.zip" \
"scrcpy-win64-$TAG.zip" > SHA256SUMS.txt
echo "Release generated in release-$TAG/"

View File

@ -6,8 +6,8 @@ android {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 30
versionCode 19 versionCode 20
versionName "1.16" versionName "1.17"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {

View File

@ -12,7 +12,7 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.16 SCRCPY_VERSION_NAME=1.17
PLATFORM=${ANDROID_PLATFORM:-30} PLATFORM=${ANDROID_PLATFORM:-30}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-30.0.0}

View File

@ -19,19 +19,21 @@ public final class CleanUp {
// not instantiable // not instantiable
} }
public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen, int displayId)
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode; throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
if (needProcess) { if (needProcess) {
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode); startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode, powerOffScreen, displayId);
} else { } else {
// There is no additional clean up to do when scrcpy dies // There is no additional clean up to do when scrcpy dies
unlinkSelf(); unlinkSelf();
} }
} }
private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException { private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode, boolean powerOffScreen,
int displayId) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf( String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode)}; restoreStayOn), String.valueOf(restoreNormalPowerMode), String.valueOf(powerOffScreen), String.valueOf(displayId)};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH); builder.environment().put("CLASSPATH", SERVER_PATH);
@ -61,6 +63,8 @@ public final class CleanUp {
boolean disableShowTouches = Boolean.parseBoolean(args[0]); boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]); int restoreStayOn = Integer.parseInt(args[1]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]); boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
boolean powerOffScreen = Boolean.parseBoolean(args[3]);
int displayId = Integer.parseInt(args[4]);
if (disableShowTouches || restoreStayOn != -1) { if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager(); ServiceManager serviceManager = new ServiceManager();
@ -76,9 +80,12 @@ public final class CleanUp {
} }
} }
if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
if (Device.isScreenOn()) { if (Device.isScreenOn()) {
if (powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(displayId);
} else if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
} }
} }

View File

@ -11,11 +11,12 @@ public final class ControlMessage {
public static final int TYPE_INJECT_SCROLL_EVENT = 3; public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4; public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5; public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6; public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7; public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_SET_CLIPBOARD = 8; public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9; public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_ROTATE_DEVICE = 10; public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11;
private int type; private int type;
private String text; private String text;
@ -71,6 +72,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createBackOrScreenOn(int action) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_BACK_OR_SCREEN_ON;
msg.action = action;
return msg;
}
public static ControlMessage createSetClipboard(String text, boolean paste) { public static ControlMessage createSetClipboard(String text, boolean paste) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD; msg.type = TYPE_SET_CLIPBOARD;

View File

@ -11,6 +11,7 @@ public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13; static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27; static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20; static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1; static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1; static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
@ -66,15 +67,18 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_SCROLL_EVENT: case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent(); msg = parseInjectScrollEvent();
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOnEvent();
break;
case ControlMessage.TYPE_SET_CLIPBOARD: case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard(); msg = parseSetClipboard();
break; break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode(); msg = parseSetScreenPowerMode();
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type); msg = ControlMessage.createEmpty(type);
@ -150,6 +154,14 @@ public class ControlMessageReader {
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
} }
private ControlMessage parseBackOrScreenOnEvent() {
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
return ControlMessage.createBackOrScreenOn(action);
}
private ControlMessage parseSetClipboard() { private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) { if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null; return null;

View File

@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit;
public class Controller { public class Controller {
private static final int DEVICE_ID_VIRTUAL = -1; private static final int DEFAULT_DEVICE_ID = 0;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
@ -45,7 +45,7 @@ public class Controller {
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords(); MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0; coords.orientation = 0;
coords.size = 1; coords.size = 0;
pointerProperties[i] = props; pointerProperties[i] = props;
pointerCoords[i] = coords; pointerCoords[i] = coords;
@ -55,7 +55,7 @@ public class Controller {
public void control() throws IOException { public void control() throws IOException {
// on start, power on the device // on start, power on the device
if (!Device.isScreenOn()) { if (!Device.isScreenOn()) {
device.injectKeycode(KeyEvent.KEYCODE_POWER); device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
// dirty hack // dirty hack
// After POWER is injected, the device is powered on asynchronously. // After POWER is injected, the device is powered on asynchronously.
@ -101,13 +101,16 @@ public class Controller {
break; break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON: case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (device.supportsInputEvents()) { if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn(); pressBackOrTurnScreenOn(msg.getAction());
} }
break; break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel(); Device.expandNotificationPanel();
break; break;
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
break;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels(); Device.collapsePanels();
break; break;
case ControlMessage.TYPE_GET_CLIPBOARD: case ControlMessage.TYPE_GET_CLIPBOARD:
@ -208,9 +211,13 @@ public class Controller {
// Right-click and middle-click only work if the source is a mouse // Right-click and middle-click only work if the source is a mouse
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0; boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN; int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
if (source != InputDevice.SOURCE_MOUSE) {
// Buttons must not be set for touch events
buttons = 0;
}
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEVICE_ID_VIRTUAL, 0, source, .obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0); 0);
return device.injectEvent(event); return device.injectEvent(event);
} }
@ -233,7 +240,7 @@ public class Controller {
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEVICE_ID_VIRTUAL, 0, .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
InputDevice.SOURCE_TOUCHSCREEN, 0); InputDevice.SOURCE_TOUCHSCREEN, 0);
return device.injectEvent(event); return device.injectEvent(event);
} }
@ -251,12 +258,22 @@ public class Controller {
}, 200, TimeUnit.MILLISECONDS); }, 200, TimeUnit.MILLISECONDS);
} }
private boolean pressBackOrTurnScreenOn() { private boolean pressBackOrTurnScreenOn(int action) {
int keycode = Device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER; if (Device.isScreenOn()) {
if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) { return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0);
}
// Screen is off
// Only press POWER on ACTION_DOWN
if (action != KeyEvent.ACTION_DOWN) {
// do nothing,
return true;
}
if (keepPowerModeOff) {
schedulePowerModeOff(); schedulePowerModeOff();
} }
return device.injectKeycode(keycode); return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER);
} }
private boolean setClipboard(String text, boolean paste) { private boolean setClipboard(String text, boolean paste) {
@ -267,7 +284,7 @@ public class Controller {
// On Android >= 7, also press the PASTE key if requested // On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.injectKeycode(KeyEvent.KEYCODE_PASTE); device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE);
} }
return ok; return ok;

View File

@ -25,6 +25,9 @@ public final class Device {
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager(); private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
public interface RotationListener { public interface RotationListener {
@ -153,12 +156,16 @@ public final class Device {
return Build.MODEL; return Build.MODEL;
} }
public static boolean supportsInputEvents(int displayId) {
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public boolean supportsInputEvents() { public boolean supportsInputEvents() {
return supportsInputEvents; return supportsInputEvents;
} }
public boolean injectEvent(InputEvent inputEvent, int mode) { public static boolean injectEvent(InputEvent inputEvent, int displayId) {
if (!supportsInputEvents()) { if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()"); throw new AssertionError("Could not inject input event if !supportsInputEvents()");
} }
@ -166,22 +173,30 @@ public final class Device {
return false; return false;
} }
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode); return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
} }
public boolean injectEvent(InputEvent event) { public boolean injectEvent(InputEvent event) {
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); return injectEvent(event, displayId);
} }
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) { public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) {
long now = SystemClock.uptimeMillis(); long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD); InputDevice.SOURCE_KEYBOARD);
return injectEvent(event); return injectEvent(event, displayId);
} }
public boolean injectKeycode(int keyCode) { public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0); return injectKeyEvent(action, keyCode, repeat, metaState, displayId);
}
public static boolean pressReleaseKeycode(int keyCode, int displayId) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId);
}
public boolean pressReleaseKeycode(int keyCode) {
return pressReleaseKeycode(keyCode, displayId);
} }
public static boolean isScreenOn() { public static boolean isScreenOn() {
@ -200,6 +215,10 @@ public final class Device {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel(); SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
} }
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
}
public static void collapsePanels() { public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels(); SERVICE_MANAGER.getStatusBarManager().collapsePanels();
} }
@ -249,6 +268,13 @@ public final class Device {
return SurfaceControl.setDisplayPowerMode(d, mode); return SurfaceControl.setDisplayPowerMode(d, mode);
} }
public static boolean powerOffScreen(int displayId) {
if (!isScreenOn()) {
return true;
}
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId);
}
/** /**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/ */

View File

@ -17,6 +17,7 @@ public class Options {
private boolean stayAwake; private boolean stayAwake;
private String codecOptions; private String codecOptions;
private String encoderName; private String encoderName;
private boolean powerOffScreenOnClose;
public Ln.Level getLogLevel() { public Ln.Level getLogLevel() {
return logLevel; return logLevel;
@ -129,4 +130,12 @@ public class Options {
public void setEncoderName(String encoderName) { public void setEncoderName(String encoderName) {
this.encoderName = encoderName; this.encoderName = encoderName;
} }
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose;
}
} }

View File

@ -82,6 +82,12 @@ public final class ScreenInfo {
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
int rotation = displayInfo.getRotation(); int rotation = displayInfo.getRotation();
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
// The user requested to lock the video orientation to the current orientation
lockedVideoOrientation = rotation;
}
Size deviceSize = displayInfo.getSize(); Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) { if (crop != null) {

View File

@ -50,7 +50,7 @@ public final class Server {
} }
} }
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true); CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true, options.getPowerOffScreenOnClose(), options.getDisplayId());
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
@ -58,12 +58,14 @@ public final class Server {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName()); options.getEncoderName());
Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (options.getControl()) { if (options.getControl()) {
final Controller controller = new Controller(device, connection); final Controller controller = new Controller(device, connection);
// asynchronous // asynchronous
startController(controller); controllerThread = startController(controller);
startDeviceMessageSender(controller.getSender()); deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() { device.setClipboardListener(new Device.ClipboardListener() {
@Override @Override
@ -79,12 +81,19 @@ public final class Server {
} catch (IOException e) { } catch (IOException e) {
// this is expected on close // this is expected on close
Ln.d("Screen streaming stopped"); Ln.d("Screen streaming stopped");
} finally {
if (controllerThread != null) {
controllerThread.interrupt();
}
if (deviceMessageSenderThread != null) {
deviceMessageSenderThread.interrupt();
}
} }
} }
} }
private static void startController(final Controller controller) { private static Thread startController(final Controller controller) {
new Thread(new Runnable() { Thread thread = new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
@ -94,11 +103,13 @@ public final class Server {
Ln.d("Controller stopped"); Ln.d("Controller stopped");
} }
} }
}).start(); });
thread.start();
return thread;
} }
private static void startDeviceMessageSender(final DeviceMessageSender sender) { private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
new Thread(new Runnable() { Thread thread = new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
@ -108,7 +119,9 @@ public final class Server {
Ln.d("Device message sender stopped"); Ln.d("Device message sender stopped");
} }
} }
}).start(); });
thread.start();
return thread;
} }
private static Options createOptions(String... args) { private static Options createOptions(String... args) {
@ -122,7 +135,7 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")"); "The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
} }
final int expectedParameters = 15; final int expectedParameters = 16;
if (args.length != expectedParameters) { if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters"); throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
} }
@ -172,6 +185,9 @@ public final class Server {
String encoderName = "-".equals(args[14]) ? null : args[14]; String encoderName = "-".equals(args[14]) ? null : args[14];
options.setEncoderName(encoderName); options.setEncoderName(encoderName);
boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
return options; return options;
} }
@ -217,7 +233,7 @@ public final class Server {
if (encoders != null && encoders.length > 0) { if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:"); Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) { for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder-name '" + encoder.getName() + "'"); Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
} }
} }
} }

View File

@ -14,7 +14,7 @@ public class ActivityManager {
private final IInterface manager; private final IInterface manager;
private Method getContentProviderExternalMethod; private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodLegacy; private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod; private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) { public ActivityManager(IInterface manager) {
@ -29,7 +29,7 @@ public class ActivityManager {
} catch (NoSuchMethodException e) { } catch (NoSuchMethodException e) {
// old version // old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class); getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodLegacy = true; getContentProviderExternalMethodNewVersion = false;
} }
} }
return getContentProviderExternalMethod; return getContentProviderExternalMethod;
@ -46,7 +46,7 @@ public class ActivityManager {
try { try {
Method method = getGetContentProviderExternalMethod(); Method method = getGetContentProviderExternalMethod();
Object[] args; Object[] args;
if (!getContentProviderExternalMethodLegacy) { if (getContentProviderExternalMethodNewVersion) {
// new version // new version
args = new Object[]{name, ServiceManager.USER_ID, token, null}; args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else { } else {

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