Compare commits

..

13 Commits

Author SHA1 Message Date
e306bffbc0 Use a callback to notify a new decoded frame
Make the decoder independant of the SDL event mechanism.
2021-02-14 15:31:31 +01:00
2512c53227 Improve error handling in screen initialization
After the struct screen is initialized, the window and the renderer are
necessarily valid, so there is no need o check in screen_destroy().
2021-02-14 14:47:49 +01:00
3cdbd00430 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-14 14:47:49 +01:00
93a8ef20a6 Log mipmaps error only if mipmaps are enabled 2021-02-14 14:47:49 +01:00
8c845efe9a Make use_opengl local
The flag is used only locally, there is no need to store it in the
screen structure.
2021-02-14 14:47:49 +01:00
bf38331c76 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-14 14:47:49 +01:00
7f6069213d Assert non-recursive usage of mutexes 2021-02-14 14:47:49 +01:00
5c4462ce70 Add mutex assertions 2021-02-14 14:47:49 +01:00
c894a0b937 Expose mutex assertions
Add a function to assert that the mutex is held (or not).
2021-02-14 14:47:49 +01:00
569e188254 Expose thread id 2021-02-14 14:47:49 +01:00
152fb0f915 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-14 14:47:49 +01:00
de3c1a7b4c 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-14 14:47:49 +01:00
e44714d2d1 Provide strdup() compat
Make strdup() available on all platforms.
2021-02-14 14:47:49 +01:00
50 changed files with 965 additions and 2037 deletions

View File

@ -198,7 +198,6 @@ If `--max-size` is also specified, resizing is applied after cropping.
To lock the orientation of the mirroring:
```bash
scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation 0 # natural orientation
scrcpy --lock-video-orientation 1 # 90° counterclockwise
scrcpy --lock-video-orientation 2 # 180°
@ -226,9 +225,7 @@ error will give the available encoders:
scrcpy --encoder _
```
### Capture
#### Recording
### Recording
It is possible to record the screen while mirroring:
@ -252,58 +249,6 @@ variation] does not impact the recorded file.
[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
#### Wireless
@ -546,6 +491,18 @@ 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
For presentations, it may be useful to show physical touches (on the physical
@ -741,10 +698,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 right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</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_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
@ -753,27 +710,18 @@ _<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 on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard⁴ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste⁴ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd>
| Collapse notification panel | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
| Copy to clipboard³ | <kbd>MOD</kbd>+<kbd>c</kbd>
| Cut to clipboard³ | <kbd>MOD</kbd>+<kbd>x</kbd>
| Synchronize clipboards and paste³ | <kbd>MOD</kbd>+<kbd>v</kbd>
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
4th and 5th mouse buttons, if your mouse has them._
_⁴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>.
Only on Android >= 7._
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
handled by the active application.

View File

@ -33,11 +33,6 @@ 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'
]
@ -54,10 +49,6 @@ if not get_option('crossbuild_windows')
dependency('sdl2'),
]
if v4l2_support
dependencies += dependency('libavdevice')
endif
else
# cross-compile mingw32 build (from Linux to Windows)
@ -108,6 +99,9 @@ foreach f : check_functions
endif
endforeach
# expose the build type
conf.set('NDEBUG', get_option('buildtype') != 'debug')
# the version, updated on release
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
@ -123,19 +117,29 @@ conf.set('PORTABLE', get_option('portable'))
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
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
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# enable High DPI support
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
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')
src_dir = include_directories('src')

View File

@ -83,12 +83,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.BI "\-\-lock\-video\-orientation " [value]
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.
.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.
Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
Default is -1 (unlocked).
.TP
.BI "\-\-max\-fps " value
@ -157,6 +155,10 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
.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
.BI "\-\-rotation " value
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
@ -186,10 +188,8 @@ Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy).
.TP
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.
It requires to lock the video orientation (see --lock-video-orientation).
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI "\-V, \-\-verbosity " value
@ -197,10 +197,6 @@ Set the log level ("debug", "info", "warn" or "error").
Default is "info" for release builds, "debug" for debug builds.
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running, when the device is plugged in.

View File

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

View File

@ -8,7 +8,4 @@
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
#define container_of(ptr, type, member) \
((type *) (((char *) (ptr)) - offsetof(type, member)))
#endif

View File

@ -8,9 +8,20 @@
# define _DARWIN_C_SOURCE
#endif
#include <libavcodec/version.h>
#include <libavformat/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:
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
// Deprecate use of av_register_input_format(), av_register_output_format(),
@ -22,6 +33,15 @@
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#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)
// <https://wiki.libsdl.org/SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH>
# define SCRCPY_SDL_HAS_HINT_MOUSE_FOCUS_CLICKTHROUGH
@ -36,7 +56,7 @@
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
#endif
#ifndef HAVE_STRDUP
#ifdef HAVE_STRDUP
char *strdup(const char *s);
#endif

View File

@ -67,9 +67,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
buffer_write32be(&buf[17],
(uint32_t) msg->inject_scroll_event.vscroll);
return 21;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
buf[1] = msg->inject_keycode.action;
return 2;
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
buf[1] = !!msg->set_clipboard.paste;
size_t len = write_string(msg->set_clipboard.text,
@ -80,9 +77,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode;
return 2;
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
case CONTROL_MSG_TYPE_ROTATE_DEVICE:
// no additional data

View File

@ -27,8 +27,7 @@ enum control_msg_type {
CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
CONTROL_MSG_TYPE_COLLAPSE_PANELS,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
CONTROL_MSG_TYPE_GET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
@ -65,10 +64,6 @@ struct control_msg {
int32_t hscroll;
int32_t vscroll;
} inject_scroll_event;
struct {
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;

View File

@ -1,43 +1,41 @@
#include "decoder.h"
#include <libavformat/avformat.h>
#include <libavutil/time.h>
#include <unistd.h>
#include "events.h"
#include "recorder.h"
#include "video_buffer.h"
#include "trait/frame_sink.h"
#include "util/buffer_util.h"
#include "util/log.h"
/** Downcast packet_sink to decoder */
#define DOWNCAST(SINK) container_of(SINK, struct decoder, packet_sink)
// set the decoded frame as ready for rendering, and notify
static void
decoder_close_first_sinks(struct decoder *decoder, unsigned count) {
while (count) {
struct sc_frame_sink *sink = decoder->sinks[--count];
sink->ops->close(sink);
}
}
static inline void
decoder_close_sinks(struct decoder *decoder) {
decoder_close_first_sinks(decoder, decoder->sink_count);
}
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;
}
push_frame(struct decoder *decoder) {
bool previous_frame_skipped;
video_buffer_offer_decoded_frame(decoder->video_buffer,
&previous_frame_skipped);
if (previous_frame_skipped) {
// the previous callback will consume this frame
return;
}
return true;
decoder->cbs->on_new_frame(decoder, decoder->cbs_userdata);
}
static bool
void
decoder_init(struct decoder *decoder, struct video_buffer *vb,
const struct decoder_callbacks *cbs, void *cbs_userdata) {
decoder->video_buffer = vb;
assert(cbs);
assert(cbs->on_new_frame);
decoder->cbs = cbs;
decoder->cbs_userdata = cbs_userdata;
}
bool
decoder_open(struct decoder *decoder, const AVCodec *codec) {
decoder->codec_ctx = avcodec_alloc_context3(codec);
if (!decoder->codec_ctx) {
@ -51,108 +49,52 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) {
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;
}
static void
void
decoder_close(struct decoder *decoder) {
decoder_close_sinks(decoder);
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
}
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
bool
decoder_push(struct decoder *decoder, const AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
if (is_config) {
// nothing to do
return true;
}
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
// the new decoding/encoding API has been introduced by:
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
#ifdef SCRCPY_LAVF_HAS_NEW_ENCODING_DECODING_API
int ret;
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) {
LOGE("Could not send video packet: %d", ret);
return false;
}
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
ret = avcodec_receive_frame(decoder->codec_ctx,
decoder->video_buffer->decoding_frame);
if (!ret) {
// a frame was received
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);
push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret);
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;
}
static bool
decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
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;
decoder_interrupt(struct decoder *decoder) {
video_buffer_interrupt(decoder->video_buffer);
}

View File

@ -3,27 +3,39 @@
#include "common.h"
#include "trait/packet_sink.h"
#include <stdbool.h>
#include <libavformat/avformat.h>
#define DECODER_MAX_SINKS 2
struct video_buffer;
struct decoder {
struct sc_packet_sink packet_sink; // packet sink trait
struct sc_frame_sink *sinks[DECODER_MAX_SINKS];
unsigned sink_count;
struct video_buffer *video_buffer;
AVCodecContext *codec_ctx;
AVFrame *frame;
const struct decoder_callbacks *cbs;
void *cbs_userdata;
};
struct decoder_callbacks {
// Called when a new frame can be consumed by
// video_buffer_take_rendering_frame(decoder->video_buffer).
void (*on_new_frame)(struct decoder *decoder, void *userdata);
};
void
decoder_init(struct decoder *decoder);
decoder_init(struct decoder *decoder, struct video_buffer *vb,
const struct decoder_callbacks *cbs, void *cbs_userdata);
bool
decoder_open(struct decoder *decoder, const AVCodec *codec);
void
decoder_add_sink(struct decoder *decoder, struct sc_frame_sink *sink);
decoder_close(struct decoder *decoder);
bool
decoder_push(struct decoder *decoder, const AVPacket *packet);
void
decoder_interrupt(struct decoder *decoder);
#endif

View File

@ -72,10 +72,6 @@ input_manager_init(struct input_manager *im,
im->sdl_shortcut_mods.count = shortcut_mods->count;
im->vfinger_down = false;
im->last_keycode = SDLK_UNKNOWN;
im->last_mod = 0;
im->key_repeat = 0;
}
static void
@ -150,25 +146,13 @@ action_cut(struct controller *controller, int actions) {
}
// 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
press_back_or_turn_screen_on(struct controller *controller, int actions) {
press_back_or_turn_screen_on(struct controller *controller) {
struct control_msg msg;
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)) {
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'");
}
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'press back or turn screen on'");
}
}
@ -183,19 +167,9 @@ expand_notification_panel(struct controller *controller) {
}
static void
expand_settings_panel(struct controller *controller) {
collapse_notification_panel(struct controller *controller) {
struct control_msg msg;
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;
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'");
@ -312,7 +286,7 @@ rotate_client_right(struct screen *screen) {
screen_set_rotation(screen, new_rotation);
}
static void
void
input_manager_process_text_input(struct input_manager *im,
const SDL_TextInputEvent *event) {
if (is_shortcut_mod(im, SDL_GetModState())) {
@ -392,33 +366,22 @@ convert_input_key(const SDL_KeyboardEvent *from, struct control_msg *to,
return true;
}
static void
void
input_manager_process_key(struct input_manager *im,
const SDL_KeyboardEvent *event) {
// control: indicates the state of the command-line option --no-control
bool control = im->control;
bool smod = is_shortcut_mod(im, event->keysym.mod);
struct controller *controller = im->controller;
SDL_Keycode keycode = event->keysym.sym;
uint16_t mod = event->keysym.mod;
bool down = event->type == SDL_KEYDOWN;
bool ctrl = event->keysym.mod & KMOD_CTRL;
bool shift = event->keysym.mod & KMOD_SHIFT;
bool repeat = event->repeat;
bool smod = is_shortcut_mod(im, mod);
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
if (smod) {
int action = down ? ACTION_DOWN : ACTION_UP;
@ -517,17 +480,17 @@ input_manager_process_key(struct input_manager *im,
return;
case SDLK_i:
if (!shift && !repeat && down) {
switch_fps_counter_state(im->fps_counter);
struct fps_counter *fps_counter =
im->video_buffer->fps_counter;
switch_fps_counter_state(fps_counter);
}
return;
case SDLK_n:
if (control && !repeat && down) {
if (shift) {
collapse_panels(controller);
} else if (im->key_repeat == 0) {
expand_notification_panel(controller);
collapse_notification_panel(controller);
} else {
expand_settings_panel(controller);
expand_notification_panel(controller);
}
}
return;
@ -588,7 +551,7 @@ convert_mouse_motion(const SDL_MouseMotionEvent *from, struct screen *screen,
return true;
}
static void
void
input_manager_process_mouse_motion(struct input_manager *im,
const SDL_MouseMotionEvent *event) {
if (!event->state) {
@ -642,7 +605,7 @@ convert_touch(const SDL_TouchFingerEvent *from, struct screen *screen,
return true;
}
static void
void
input_manager_process_touch(struct input_manager *im,
const SDL_TouchFingerEvent *event) {
struct control_msg msg;
@ -674,7 +637,7 @@ convert_mouse_button(const SDL_MouseButtonEvent *from, struct screen *screen,
return true;
}
static void
void
input_manager_process_mouse_button(struct input_manager *im,
const SDL_MouseButtonEvent *event) {
bool control = im->control;
@ -685,27 +648,13 @@ input_manager_process_mouse_button(struct input_manager *im,
}
bool down = event->type == SDL_MOUSEBUTTONDOWN;
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 (!im->forward_all_clicks && down) {
if (control && event->button == SDL_BUTTON_RIGHT) {
press_back_or_turn_screen_on(im->controller, action);
press_back_or_turn_screen_on(im->controller);
return;
}
if (control && event->button == SDL_BUTTON_MIDDLE) {
action_home(im->controller, action);
action_home(im->controller, ACTION_DOWN | ACTION_UP);
return;
}
@ -718,9 +667,7 @@ input_manager_process_mouse_button(struct input_manager *im,
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
if (down) {
screen_resize_to_fit(im->screen);
}
screen_resize_to_fit(im->screen);
return;
}
}
@ -789,7 +736,7 @@ convert_mouse_wheel(const SDL_MouseWheelEvent *from, struct screen *screen,
return true;
}
static void
void
input_manager_process_mouse_wheel(struct input_manager *im,
const SDL_MouseWheelEvent *event) {
struct control_msg msg;
@ -799,46 +746,3 @@ 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

@ -11,10 +11,11 @@
#include "fps_counter.h"
#include "scrcpy.h"
#include "screen.h"
#include "video_buffer.h"
struct input_manager {
struct controller *controller;
struct fps_counter *fps_counter;
struct video_buffer *video_buffer;
struct screen *screen;
// SDL reports repeated events as a boolean, but Android expects the actual
@ -33,20 +34,34 @@ struct input_manager {
} sdl_shortcut_mods;
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
input_manager_init(struct input_manager *im,
const struct scrcpy_options *options);
bool
input_manager_handle_event(struct input_manager *im, SDL_Event *event);
void
input_manager_process_text_input(struct input_manager *im,
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

View File

@ -6,9 +6,6 @@
#include <stdbool.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
#include <SDL2/SDL.h>
@ -31,11 +28,6 @@ print_version(void) {
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
LIBAVUTIL_VERSION_MINOR,
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
@ -98,12 +90,6 @@ main(int argc, char *argv[]) {
av_register_all();
#endif
#ifdef HAVE_V4L2
if (args.opts.v4l2_device) {
avdevice_register_all();
}
#endif
if (avformat_network_init()) {
return 1;
}

View File

@ -4,10 +4,6 @@
#include <libavutil/time.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
@ -23,8 +19,8 @@ find_muxer(const char *name) {
#else
oformat = av_oformat_next(oformat);
#endif
// until null or containing the requested name
} while (oformat && !strlist_contains(oformat->name, ',', name));
// until null or with name "mp4"
} while (oformat && strcmp(oformat->name, name));
return oformat;
}
@ -61,6 +57,50 @@ 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 = strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
return false;
}
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
LOGC("Could not create mutex");
free(recorder->filename);
return false;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
LOGC("Could not create cond");
sc_mutex_destroy(&recorder->mutex);
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) {
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
free(recorder->filename);
}
static const char *
recorder_get_format_name(enum sc_record_format format) {
switch (format) {
@ -70,6 +110,88 @@ 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
recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
@ -83,8 +205,13 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
// copy the first packet to the extra data
memcpy(extradata, packet->data, packet->size);
#ifdef SCRCPY_LAVF_HAS_NEW_CODEC_PARAMS_API
ostream->codecpar->extradata = extradata;
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);
if (ret < 0) {
@ -101,7 +228,7 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
static bool
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
@ -190,26 +317,7 @@ run_recorder(void *data) {
sc_mutex_unlock(&recorder->mutex);
break;
}
}
if (!recorder->failed) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
}
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
LOGD("Recorder thread ended");
@ -217,108 +325,34 @@ run_recorder(void *data) {
return 0;
}
static bool
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
LOGC("Could not create mutex");
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;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
recorder);
if (!ok) {
LOGC("Could not start recorder thread");
goto error_avio_close;
return false;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
error_avio_close:
avio_close(recorder->ctx->pb);
error_avformat_free_context:
avformat_free_context(recorder->ctx);
error_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
return false;
}
static void
recorder_close(struct recorder *recorder) {
void
recorder_stop(struct recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
sc_thread_join(&recorder->thread, NULL);
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
}
static bool
void
recorder_join(struct recorder *recorder) {
sc_thread_join(&recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
sc_mutex_lock(&recorder->mutex);
assert(!recorder->stopped);
@ -342,51 +376,3 @@ recorder_push(struct recorder *recorder, const AVPacket *packet) {
sc_mutex_unlock(&recorder->mutex);
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

@ -8,7 +8,6 @@
#include "coords.h"
#include "scrcpy.h"
#include "trait/packet_sink.h"
#include "util/queue.h"
#include "util/thread.h"
@ -20,8 +19,6 @@ struct record_packet {
struct recorder_queue QUEUE(struct record_packet);
struct recorder {
struct sc_packet_sink packet_sink; // packet sink trait
char *filename;
enum sc_record_format format;
AVFormatContext *ctx;
@ -31,7 +28,7 @@ struct recorder {
sc_thread thread;
sc_mutex mutex;
sc_cond queue_cond;
bool stopped; // set on recorder_close()
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
@ -49,4 +46,22 @@ recorder_init(struct recorder *recorder, const char *filename,
void
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

View File

@ -25,27 +25,23 @@
#include "server.h"
#include "stream.h"
#include "tiny_xpm.h"
#include "video_buffer.h"
#include "util/log.h"
#include "util/net.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
static struct server server;
static struct screen screen;
static struct screen screen = SCREEN_INITIALIZER;
static struct fps_counter fps_counter;
static struct video_buffer video_buffer;
static struct stream stream;
static struct decoder decoder;
static struct recorder recorder;
#ifdef HAVE_V4L2
static struct sc_v4l2_sink v4l2_sink;
#endif
static struct controller controller;
static struct file_handler file_handler;
static struct input_manager input_manager = {
.controller = &controller,
.fps_counter = &fps_counter,
.video_buffer = &video_buffer,
.screen = &screen,
.repeat = 0,
@ -132,6 +128,30 @@ sdl_init_and_configure(bool display, const char *render_driver,
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
is_apk(const char *file) {
const char *ext = strrchr(file, '.');
@ -153,6 +173,56 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
case SDL_QUIT:
LOGD("User requested to quit");
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:
if (screen.has_frame) {
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: {
if (!options->control) {
break;
@ -171,24 +241,19 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
action = ACTION_PUSH_FILE;
}
file_handler_request(&file_handler, action, file);
goto end;
break;
}
}
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;
}
static bool
event_loop(const struct scrcpy_options *options) {
#ifdef CONTINUOUS_RESIZING_WORKAROUND
if (options->display) {
SDL_AddEventWatch(event_watcher, NULL);
}
#endif
SDL_Event event;
while (SDL_WaitEvent(&event)) {
enum event_result result = handle_event(&event, options);
@ -241,25 +306,36 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
free(local_fmt);
}
static void
decoder_on_new_frame(struct decoder *decoder, void *userdata) {
(void) decoder;
(void) userdata;
static SDL_Event new_frame_event = {
.type = EVENT_NEW_FRAME,
};
SDL_PushEvent(&new_frame_event);
}
bool
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 video_buffer_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;
struct server_params params = {
.serial = options->serial,
.log_level = options->log_level,
.crop = options->crop,
.port_range = options->port_range,
@ -274,14 +350,8 @@ scrcpy(const struct scrcpy_options *options) {
.codec_options = options->codec_options,
.encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
};
if (!server_init(&server, &params)) {
return false;
}
if (!server_start(&server)) {
if (!server_start(&server, options->serial, &params)) {
goto end;
}
@ -306,28 +376,32 @@ scrcpy(const struct scrcpy_options *options) {
goto end;
}
struct decoder *dec = NULL;
if (options->display) {
if (!fps_counter_init(&fps_counter)) {
goto end;
}
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 (!file_handler_init(&file_handler, options->serial,
if (!file_handler_init(&file_handler, server.serial,
options->push_target)) {
goto end;
}
file_handler_initialized = true;
}
}
struct decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
decoder_init(&decoder);
static const struct decoder_callbacks decoder_cbs = {
.on_new_frame = decoder_on_new_frame,
};
decoder_init(&decoder, &video_buffer, &decoder_cbs, NULL);
dec = &decoder;
}
@ -345,15 +419,14 @@ scrcpy(const struct scrcpy_options *options) {
av_log_set_callback(av_log_callback);
stream_init(&stream, server.video_socket);
stream_init(&stream, server.video_socket, dec, rec);
if (dec) {
stream_add_sink(&stream, &dec->packet_sink);
}
if (rec) {
stream_add_sink(&stream, &rec->packet_sink);
// now we consumed the header values, the socket receives the video stream
// start the stream
if (!stream_start(&stream)) {
goto end;
}
stream_started = true;
if (options->display) {
if (options->control) {
@ -371,26 +444,14 @@ scrcpy(const struct scrcpy_options *options) {
const char *window_title =
options->window_title ? options->window_title : device_name;
struct screen_params screen_params = {
.window_title = window_title,
.frame_size = frame_size,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
.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)) {
if (!screen_init_rendering(&screen, window_title, frame_size,
options->always_on_top, options->window_x,
options->window_y, options->window_width,
options->window_height,
options->window_borderless,
options->rotation, options->mipmaps)) {
goto end;
}
screen_initialized = true;
decoder_add_sink(&decoder, &screen.frame_sink);
if (options->turn_screen_off) {
struct control_msg msg;
@ -401,39 +462,25 @@ scrcpy(const struct scrcpy_options *options) {
LOGW("Could not request 'set screen power mode'");
}
}
}
#ifdef HAVE_V4L2
if (options->v4l2_device) {
if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) {
goto end;
if (options->fullscreen) {
screen_switch_fullscreen(&screen);
}
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);
ret = event_loop(options);
LOGD("quit...");
// 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);
screen_destroy(&screen);
end:
// The stream is not stopped explicitly, because it will stop by itself on
// end-of-stream
// stop stream and controller so that they don't continue once their socket
// is shutdown
if (stream_started) {
stream_stop(&stream);
}
if (controller_started) {
controller_stop(&controller);
}
@ -454,19 +501,6 @@ end:
if (stream_started) {
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) {
controller_join(&controller);
}
@ -483,6 +517,10 @@ end:
file_handler_destroy(&file_handler);
}
if (video_buffer_initialized) {
video_buffer_destroy(&video_buffer);
}
if (fps_counter_initialized) {
fps_counter_join(&fps_counter);
fps_counter_destroy(&fps_counter);

View File

@ -20,16 +20,6 @@ enum sc_record_format {
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
enum sc_shortcut_mod {
@ -62,7 +52,6 @@ struct scrcpy_options {
const char *render_driver;
const char *codec_options;
const char *encoder_name;
const char *v4l2_device;
enum sc_log_level log_level;
enum sc_record_format record_format;
struct sc_port_range port_range;
@ -70,7 +59,7 @@ struct scrcpy_options {
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
enum sc_lock_video_orientation lock_video_orientation;
int8_t lock_video_orientation;
uint8_t rotation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
@ -83,6 +72,7 @@ struct scrcpy_options {
bool control;
bool display;
bool turn_screen_off;
bool render_expired_frames;
bool prefer_text;
bool window_borderless;
bool mipmaps;
@ -92,7 +82,6 @@ struct scrcpy_options {
bool forward_key_repeat;
bool forward_all_clicks;
bool legacy_paste;
bool power_off_on_close;
};
#define SCRCPY_OPTIONS_DEFAULT { \
@ -104,7 +93,6 @@ struct scrcpy_options {
.render_driver = NULL, \
.codec_options = NULL, \
.encoder_name = NULL, \
.v4l2_device = NULL, \
.log_level = SC_LOG_LEVEL_INFO, \
.record_format = SC_RECORD_FORMAT_AUTO, \
.port_range = { \
@ -115,10 +103,10 @@ struct scrcpy_options {
.data = {SC_MOD_LALT, SC_MOD_LSUPER}, \
.count = 2, \
}, \
.max_size = 0, \
.max_size = DEFAULT_MAX_SIZE, \
.bit_rate = DEFAULT_BIT_RATE, \
.max_fps = 0, \
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
.lock_video_orientation = DEFAULT_LOCK_VIDEO_ORIENTATION, \
.rotation = 0, \
.window_x = SC_WINDOW_POSITION_UNDEFINED, \
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
@ -131,6 +119,7 @@ struct scrcpy_options {
.control = true, \
.display = true, \
.turn_screen_off = false, \
.render_expired_frames = false, \
.prefer_text = false, \
.window_borderless = false, \
.mipmaps = true, \
@ -140,7 +129,6 @@ struct scrcpy_options {
.forward_key_repeat = true, \
.forward_all_clicks = false, \
.legacy_paste = false, \
.power_off_on_close = false, \
}
bool

View File

@ -4,7 +4,6 @@
#include <string.h>
#include <SDL2/SDL.h>
#include "events.h"
#include "icon.xpm"
#include "scrcpy.h"
#include "tiny_xpm.h"
@ -13,8 +12,6 @@
#define DISPLAY_MARGINS 96
#define DOWNCAST(SINK) container_of(SINK, struct screen, frame_sink)
static inline struct size
get_rotated_size(struct size size, int rotation) {
struct size rotated_size;
@ -193,6 +190,11 @@ screen_update_content_rect(struct screen *screen) {
}
}
void
screen_init(struct screen *screen) {
*screen = (struct screen) SCREEN_INITIALIZER;
}
static inline SDL_Texture *
create_texture(struct screen *screen) {
SDL_Renderer *renderer = screen->renderer;
@ -220,110 +222,27 @@ create_texture(struct screen *screen) {
return texture;
}
#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) {
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);
}
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_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top,
int16_t window_x, int16_t window_y, uint16_t window_width,
uint16_t window_height, bool window_borderless,
uint8_t rotation, bool mipmaps) {
screen->frame_size = frame_size;
screen->rotation = rotation;
if (rotation) {
LOGI("Initial display rotation set to %u", rotation);
}
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);
struct size content_size = get_rotated_size(frame_size, screen->rotation);
screen->content_size = content_size;
struct size window_size = get_initial_optimal_size(content_size,
params->window_width,
params->window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE
| SDL_WINDOW_ALLOW_HIGHDPI;
if (params->always_on_top) {
struct size window_size =
get_initial_optimal_size(content_size, window_width, window_height);
uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE;
#ifdef HIDPI_SUPPORT
window_flags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif
if (always_on_top) {
#ifdef SCRCPY_SDL_HAS_WINDOW_ALWAYS_ON_TOP
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
@ -331,15 +250,15 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
"(compile with SDL >= 2.0.5 to enable it)");
#endif
}
if (params->window_borderless) {
if (window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED
? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED
? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(params->window_title, x, y,
int x = window_x != SC_WINDOW_POSITION_UNDEFINED
? window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = window_y != SC_WINDOW_POSITION_UNDEFINED
? window_y : (int) SDL_WINDOWPOS_UNDEFINED;
screen->window = SDL_CreateWindow(window_title, x, y,
window_size.width, window_size.height,
window_flags);
if (!screen->window) {
@ -352,7 +271,6 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
if (!screen->renderer) {
LOGC("Could not create renderer: %s", SDL_GetError());
SDL_DestroyWindow(screen->window);
video_buffer_destroy(&screen->vb);
return false;
}
@ -361,8 +279,6 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
screen->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
@ -371,7 +287,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
LOGI("OpenGL version: %s", gl->version);
if (params->mipmaps) {
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
@ -385,7 +301,7 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
} else {
LOGI("Trilinear filtering disabled");
}
} else if (params->mipmaps) {
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
@ -397,24 +313,14 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
LOGW("Could not load icon");
}
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width,
params->frame_size.height);
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width,
frame_size.height);
screen->texture = create_texture(screen);
if (!screen->texture) {
LOGC("Could not create texture: %s", SDL_GetError());
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);
screen_destroy(screen);
return false;
}
@ -425,49 +331,21 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
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;
}
static void
void
screen_show_window(struct screen *screen) {
SDL_ShowWindow(screen->window);
}
void
screen_hide_window(struct screen *screen) {
SDL_HideWindow(screen->window);
}
void
screen_destroy(struct screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
av_frame_free(&screen->frame);
SDL_DestroyTexture(screen->texture);
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
video_buffer_destroy(&screen->vb);
}
static void
@ -570,14 +448,9 @@ update_texture(struct screen *screen, const AVFrame *frame) {
}
}
static bool
screen_update_frame(struct screen *screen) {
av_frame_unref(screen->frame);
video_buffer_consume(&screen->vb, screen->frame);
AVFrame *frame = screen->frame;
fps_counter_add_rendered_frame(screen->fps_counter);
bool
screen_update_frame(struct screen *screen, struct video_buffer *vb) {
const AVFrame *frame = video_buffer_take_rendering_frame(vb);
struct size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) {
return false;
@ -669,52 +542,31 @@ screen_resize_to_pixel_perfect(struct screen *screen) {
content_size.height);
}
bool
screen_handle_event(struct screen *screen, SDL_Event *event) {
switch (event->type) {
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);
void
screen_handle_window_event(struct screen *screen,
const SDL_WindowEvent *event) {
switch (event->event) {
case SDL_WINDOWEVENT_EXPOSED:
screen_render(screen, true);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(screen, true);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling fullscreen
// mode unexpectedly triggers the "restored" then "maximized"
// events, leaving the window in a weird state (maximized
// according to the events, but not maximized visually).
break;
}
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:
screen_render(screen, true);
break;
case SDL_WINDOWEVENT_SIZE_CHANGED:
screen_render(screen, true);
break;
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling
// fullscreen mode unexpectedly triggers the "restored"
// then "maximized" events, leaving the window in a
// weird state (maximized according to the events, but
// not maximized visually).
break;
}
screen->maximized = false;
apply_pending_resize(screen);
break;
}
return true;
screen->maximized = false;
apply_pending_resize(screen);
break;
}
return false;
}
struct point

View File

@ -9,19 +9,10 @@
#include "coords.h"
#include "opengl.h"
#include "trait/frame_sink.h"
#include "video_buffer.h"
struct video_buffer;
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_Renderer *renderer;
SDL_Texture *texture;
@ -41,44 +32,66 @@ struct screen {
bool has_frame;
bool fullscreen;
bool maximized;
bool no_window;
bool mipmaps;
AVFrame *frame;
};
struct screen_params {
const char *window_title;
struct size frame_size;
bool always_on_top;
#define SCREEN_INITIALIZER { \
.window = NULL, \
.renderer = NULL, \
.texture = NULL, \
.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, \
}
int16_t window_x;
int16_t window_y;
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 default values
void
screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden)
// window_x and window_y accept SC_WINDOW_POSITION_UNDEFINED
bool
screen_init(struct screen *screen, struct fps_counter *fps_counter,
const struct screen_params *params);
screen_init_rendering(struct screen *screen, const char *window_title,
struct size frame_size, bool always_on_top,
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)
void
screen_destroy(struct screen *screen);
// hide the window
//
// It is used to hide the window immediately on closing without waiting for
// screen_destroy()
void
screen_hide_window(struct screen *screen);
// resize if necessary and write the rendered frame into the texture
bool
screen_update_frame(struct screen *screen, struct video_buffer *vb);
// render the texture to the renderer
//
@ -103,9 +116,9 @@ screen_resize_to_pixel_perfect(struct screen *screen);
void
screen_set_rotation(struct screen *screen, unsigned rotation);
// react to SDL events
bool
screen_handle_event(struct screen *screen, SDL_Event *event);
// react to window events
void
screen_handle_window_event(struct screen *screen, const SDL_WindowEvent *event);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels

View File

@ -58,7 +58,7 @@ get_server_path(void) {
LOGE("Could not get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return strdup(SERVER_FILENAME);
return SERVER_FILENAME;
}
char *dir = dirname(executable_path);
size_t dirlen = strlen(dir);
@ -70,7 +70,7 @@ get_server_path(void) {
LOGE("Could not alloc server path string, "
"using " SERVER_FILENAME " from current directory");
free(executable_path);
return strdup(SERVER_FILENAME);
return SERVER_FILENAME;
}
memcpy(server_path, dir, dirlen);
@ -128,10 +128,9 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
static bool
disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
return disable_tunnel_forward(server->params.serial,
server->local_port);
return disable_tunnel_forward(server->serial, server->local_port);
}
return disable_tunnel_reverse(server->params.serial);
return disable_tunnel_reverse(server->serial);
}
static socket_t
@ -145,7 +144,7 @@ enable_tunnel_reverse_any_port(struct server *server,
struct sc_port_range port_range) {
uint16_t port = port_range.first;
for (;;) {
if (!enable_tunnel_reverse(server->params.serial, port)) {
if (!enable_tunnel_reverse(server->serial, port)) {
// the command itself failed, it will fail on any port
return false;
}
@ -164,7 +163,7 @@ enable_tunnel_reverse_any_port(struct server *server,
}
// failure, disable tunnel and try another port
if (!disable_tunnel_reverse(server->params.serial)) {
if (!disable_tunnel_reverse(server->serial)) {
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
}
@ -192,7 +191,7 @@ enable_tunnel_forward_any_port(struct server *server,
server->tunnel_forward = true;
uint16_t port = port_range.first;
for (;;) {
if (enable_tunnel_forward(server->params.serial, port)) {
if (enable_tunnel_forward(server->serial, port)) {
// success
server->local_port = port;
return true;
@ -294,7 +293,6 @@ execute_server(struct server *server, const struct server_params *params) {
params->stay_awake ? "true" : "false",
params->codec_options ? params->codec_options : "-",
params->encoder_name ? params->encoder_name : "-",
params->power_off_on_close ? "true" : "false",
};
#ifdef SERVER_DEBUGGER
LOGI("Server debugger waiting for a client on device port "
@ -307,7 +305,7 @@ execute_server(struct server *server, const struct server_params *params) {
// Port: 5005
// Then click on "Debug"
#endif
return adb_execute(server->params.serial, cmd, ARRAY_LEN(cmd));
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
}
static socket_t
@ -353,75 +351,21 @@ close_socket(socket_t socket) {
}
}
static void
server_params_destroy(struct server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->crop);
free((char *) params->codec_options);
free((char *) params->encoder_name);
}
static bool
server_params_copy(struct server_params *dst, const struct server_params *src) {
// params reference user-allocated memory, so we must copy them to handle
// them from a separate thread
*dst = *src;
dst->crop = NULL;
dst->codec_options = NULL;
dst->encoder_name = NULL;
if (src->crop) {
dst->crop = strdup(src->crop);
if (!dst->crop) {
goto error;
}
}
if (src->codec_options) {
dst->codec_options = strdup(src->codec_options);
if (!dst->codec_options) {
goto error;
}
}
if (src->encoder_name) {
dst->encoder_name = strdup(src->encoder_name);
if (!dst->encoder_name) {
goto error;
}
}
return true;
error:
server_params_destroy(dst);
return false;
};
bool
server_init(struct server *server, const struct server_params *params) {
if (!server_params_copy(&server->params, params)) {
LOGE("Could not copy server params");
return false;
}
server_init(struct server *server) {
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) {
server_params_destroy(&server->params);
return false;
}
ok = sc_cond_init(&server->process_terminated_cond);
if (!ok) {
sc_mutex_destroy(&server->mutex);
server_params_destroy(&server->params);
return false;
}
@ -461,41 +405,31 @@ run_wait_server(void *data) {
return 0;
}
static int
run_server(void *data) {
struct server *server = data;
bool
server_start(struct server *server, const char *serial,
const struct server_params *params) {
if (serial) {
server->serial = strdup(serial);
if (!server->serial) {
return false;
}
}
const struct server_params *params = &server->params;
const struct server_callbacks *cbs = &server->cbs;
void *userdata = server->userdata;
if (!push_server(params->serial)) {
cbs->on_connection_failed(server);
goto end;
if (!push_server(serial)) {
goto error1;
}
if (!enable_tunnel_any_port(server, params->port_range,
params->force_adb_forward)) {
cbs->on_connection_failed(server);
goto end;
goto error1;
}
// server will connect to our server socket
server->process = execute_server(server, params);
if (server->process == PROCESS_NONE) {
cbs->on_connection_failed(server);
goto end;
goto error2;
}
process_wait(server->process, false); // ignore exit code
end:
return 0;
}
bool
server_start(struct server *server) {
// If the server process dies before connecting to the server socket, then
// the client will be stuck forever on accept(). To avoid the problem, we
// must be able to wake up the accept() call when the server dies. To keep
@ -507,14 +441,14 @@ server_start(struct server *server) {
if (!ok) {
process_terminate(server->process);
process_wait(server->process, true); // ignore exit code
goto error;
goto error2;
}
server->tunnel_enabled = true;
return true;
error:
error2:
if (!server->tunnel_forward) {
bool was_closed =
atomic_flag_test_and_set(&server->server_socket_closed);
@ -524,7 +458,8 @@ error:
close_socket(server->server_socket);
}
disable_tunnel(server);
error1:
free(server->serial);
return false;
}
@ -622,5 +557,4 @@ server_destroy(struct server *server) {
free(server->serial);
sc_cond_destroy(&server->process_terminated_cond);
sc_mutex_destroy(&server->mutex);
server_params_destroy(&server->params);
}

View File

@ -13,25 +13,6 @@
#include "util/net.h"
#include "util/thread.h"
struct server_params {
enum sc_log_level log_level;
const char *serial;
const char *crop;
const char *codec_options;
const char *encoder_name;
struct sc_port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
bool power_off_on_close;
};
struct server {
char *serial;
process_t process;
@ -48,29 +29,33 @@ struct server {
uint16_t local_port; // selected from port_range
bool tunnel_enabled;
bool tunnel_forward; // use "adb forward" instead of "adb reverse"
// The internal allocated strings are copies owned by the server
struct server_params params;
const struct server_callbacks *cbs;
void *userdata;
};
struct server_callbacks {
void (*on_connection_failed)(struct server *server);
void (*on_connected)(struct server *server, const char *name,
struct size size, void *userdata);
void (*on_disconnected)(struct server *server, void *userdata);
struct server_params {
enum sc_log_level log_level;
const char *crop;
const char *codec_options;
const char *encoder_name;
struct sc_port_range port_range;
uint16_t max_size;
uint32_t bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
};
// init server fields
// init default values
bool
server_init(struct server *server, const struct server_params *params);
server_init(struct server *server);
// push, enable tunnel et start the server
bool
server_start(struct server *server, const struct server_callbacks *cbs,
void *userdata);
server_start(struct server *server, const char *serial,
const struct server_params *params);
// block until the communication with the server is established
bool

View File

@ -66,11 +66,25 @@ notify_stopped(void) {
}
static bool
push_packet_to_sinks(struct stream *stream, const AVPacket *packet) {
for (unsigned i = 0; i < stream->sink_count; ++i) {
struct sc_packet_sink *sink = stream->sinks[i];
if (!sink->ops->push(sink, packet)) {
LOGE("Could not send config packet to sink %d", i);
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
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;
}
}
@ -97,11 +111,9 @@ stream_parse(struct stream *stream, AVPacket *packet) {
packet->flags |= AV_PKT_FLAG_KEY;
}
packet->dts = packet->pts;
bool ok = push_packet_to_sinks(stream, packet);
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process packet");
LOGE("Could not process frame");
return false;
}
@ -144,7 +156,7 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
if (is_config) {
// config packet
bool ok = push_packet_to_sinks(stream, packet);
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
@ -165,33 +177,6 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
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
run_stream(void *data) {
struct stream *stream = data;
@ -208,15 +193,27 @@ run_stream(void *data) {
goto end;
}
if (!stream_open_sinks(stream, codec)) {
LOGE("Could not open stream sinks");
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
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);
if (!stream->parser) {
LOGE("Could not initialize parser");
goto finally_close_sinks;
goto finally_stop_and_join_recorder;
}
// We must only pass complete frames to av_parser_parse2()!
@ -246,8 +243,20 @@ run_stream(void *data) {
}
av_parser_close(stream->parser);
finally_close_sinks:
stream_close_sinks(stream);
finally_stop_and_join_recorder:
if (stream->recorder) {
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:
avcodec_free_context(&stream->codec_ctx);
end:
@ -256,18 +265,12 @@ end:
}
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->decoder = decoder,
stream->recorder = recorder;
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
@ -282,6 +285,13 @@ stream_start(struct stream *stream) {
return true;
}
void
stream_stop(struct stream *stream) {
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}
}
void
stream_join(struct stream *stream) {
sc_thread_join(&stream->thread, NULL);

View File

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

View File

@ -1,26 +0,0 @@
#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

@ -1,27 +0,0 @@
#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

@ -140,24 +140,6 @@ parse_integer_with_suffix(const char *s, long *out) {
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
utf8_truncation_index(const char *utf8, size_t max_len) {
size_t len = strlen(utf8);

View File

@ -43,11 +43,6 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out);
bool
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
size_t
utf8_truncation_index(const char *utf8, size_t max_len);

View File

@ -1,342 +0,0 @@
#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);
}

View File

@ -1,39 +0,0 @@
#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

@ -7,82 +7,133 @@
#include "util/log.h"
bool
video_buffer_init(struct video_buffer *vb) {
vb->pending_frame = av_frame_alloc();
if (!vb->pending_frame) {
return false;
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
bool render_expired_frames) {
vb->fps_counter = fps_counter;
vb->decoding_frame = av_frame_alloc();
if (!vb->decoding_frame) {
goto error_0;
}
vb->tmp_frame = av_frame_alloc();
if (!vb->tmp_frame) {
av_frame_free(&vb->pending_frame);
return false;
vb->pending_frame = av_frame_alloc();
if (!vb->pending_frame) {
goto error_1;
}
vb->rendering_frame = av_frame_alloc();
if (!vb->rendering_frame) {
goto error_2;
}
bool ok = sc_mutex_init(&vb->mutex);
if (!ok) {
av_frame_free(&vb->pending_frame);
av_frame_free(&vb->tmp_frame);
return false;
goto error_3;
}
// there is initially no frame, so consider it has already been consumed
vb->render_expired_frames = render_expired_frames;
if (render_expired_frames) {
ok = sc_cond_init(&vb->pending_frame_consumed_cond);
if (!ok) {
sc_mutex_destroy(&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->pending_frame_consumed = true;
return true;
error_3:
av_frame_free(&vb->rendering_frame);
error_2:
av_frame_free(&vb->pending_frame);
error_1:
av_frame_free(&vb->decoding_frame);
error_0:
return false;
}
void
video_buffer_destroy(struct video_buffer *vb) {
if (vb->render_expired_frames) {
sc_cond_destroy(&vb->pending_frame_consumed_cond);
}
sc_mutex_destroy(&vb->mutex);
av_frame_free(&vb->rendering_frame);
av_frame_free(&vb->pending_frame);
av_frame_free(&vb->tmp_frame);
av_frame_free(&vb->decoding_frame);
}
static inline void
swap_frames(AVFrame **lhs, AVFrame **rhs) {
AVFrame *tmp = *lhs;
*lhs = *rhs;
*rhs = tmp;
static void
video_buffer_swap_decoding_frame(struct video_buffer *vb) {
sc_mutex_assert(&vb->mutex);
AVFrame *tmp = vb->decoding_frame;
vb->decoding_frame = vb->pending_frame;
vb->pending_frame = tmp;
}
bool
video_buffer_push(struct video_buffer *vb, const AVFrame *frame,
bool *previous_frame_skipped) {
sc_mutex_lock(&vb->mutex);
// Use a temporary frame to preserve pending_frame in case of error.
// tmp_frame is an empty frame, no need to call av_frame_unref() beforehand.
int r = av_frame_ref(vb->tmp_frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
return false;
}
// 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);
if (previous_frame_skipped) {
*previous_frame_skipped = !vb->pending_frame_consumed;
}
vb->pending_frame_consumed = false;
sc_mutex_unlock(&vb->mutex);
return true;
static void
video_buffer_swap_rendering_frame(struct video_buffer *vb) {
sc_mutex_assert(&vb->mutex);
AVFrame *tmp = vb->rendering_frame;
vb->rendering_frame = vb->pending_frame;
vb->pending_frame = tmp;
}
void
video_buffer_consume(struct video_buffer *vb, AVFrame *dst) {
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped) {
sc_mutex_lock(&vb->mutex);
if (vb->render_expired_frames) {
// wait for the current (expired) frame to be consumed
while (!vb->pending_frame_consumed && !vb->interrupted) {
sc_cond_wait(&vb->pending_frame_consumed_cond, &vb->mutex);
}
} else if (!vb->pending_frame_consumed) {
fps_counter_add_skipped_frame(vb->fps_counter);
}
video_buffer_swap_decoding_frame(vb);
*previous_frame_skipped = !vb->pending_frame_consumed;
vb->pending_frame_consumed = false;
sc_mutex_unlock(&vb->mutex);
}
const AVFrame *
video_buffer_take_rendering_frame(struct video_buffer *vb) {
sc_mutex_lock(&vb->mutex);
assert(!vb->pending_frame_consumed);
vb->pending_frame_consumed = true;
av_frame_move_ref(dst, vb->pending_frame);
// av_frame_move_ref() resets its source frame, so no need to call
// av_frame_unref()
fps_counter_add_rendered_frame(vb->fps_counter);
video_buffer_swap_rendering_frame(vb);
if (vb->render_expired_frames) {
// unblock video_buffer_offer_decoded_frame()
sc_cond_signal(&vb->pending_frame_consumed_cond);
}
sc_mutex_unlock(&vb->mutex);
// rendering_frame is only written from this thread, no need to lock
return vb->rendering_frame;
}
void
video_buffer_interrupt(struct video_buffer *vb) {
if (vb->render_expired_frames) {
sc_mutex_lock(&vb->mutex);
vb->interrupted = true;
sc_mutex_unlock(&vb->mutex);
// wake up blocking wait
sc_cond_signal(&vb->pending_frame_consumed_cond);
}
}

View File

@ -12,39 +12,57 @@
typedef struct AVFrame AVFrame;
/**
* A video buffer holds 1 pending frame, which is the last frame received from
* the producer (typically, the decoder).
* There are 3 frames in memory:
* - one frame is held by the decoder (decoding_frame)
* - one frame is held by the renderer (rendering_frame)
* - one frame is shared between the decoder and the renderer (pending_frame)
*
* 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 decoder decodes a packet into the decoding_frame (it may takes time).
*
* 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.
* Once the frame is decoded, it calls video_buffer_offer_decoded_frame(),
* which swaps the decoding and pending frames.
*
* When the renderer is notified that a new frame is available, it calls
* video_buffer_take_rendering_frame() to retrieve it, which swaps the pending
* and rendering frames. The frame is valid until the next call, without
* blocking the decoder.
*/
struct video_buffer {
AVFrame *decoding_frame;
AVFrame *pending_frame;
AVFrame *tmp_frame; // To preserve the pending frame on error
AVFrame *rendering_frame;
sc_mutex mutex;
bool render_expired_frames;
bool interrupted;
sc_cond pending_frame_consumed_cond;
bool pending_frame_consumed;
struct fps_counter *fps_counter;
};
bool
video_buffer_init(struct video_buffer *vb);
video_buffer_init(struct video_buffer *vb, struct fps_counter *fps_counter,
bool render_expired_frames);
void
video_buffer_destroy(struct video_buffer *vb);
bool
video_buffer_push(struct video_buffer *vb, const AVFrame *frame, bool *skipped);
// set the decoded frame as ready for rendering
// the output flag is set to report whether the previous frame has been skipped
void
video_buffer_consume(struct video_buffer *vb, AVFrame *dst);
video_buffer_offer_decoded_frame(struct video_buffer *vb,
bool *previous_frame_skipped);
// mark the rendering frame as consumed and return it
// the frame is valid until the next call to this function
const AVFrame *
video_buffer_take_rendering_frame(struct video_buffer *vb);
// wake up and avoid any blocking call
void
video_buffer_interrupt(struct video_buffer *vb);
#endif

View File

@ -58,6 +58,7 @@ static void test_options(void) {
"--push-target", "/sdcard/Movies",
"--record", "file",
"--record-format", "mkv",
"--render-expired-frames",
"--serial", "0123456789abcdef",
"--show-touches",
"--turn-screen-off",
@ -86,6 +87,7 @@ static void test_options(void) {
assert(!strcmp(opts->push_target, "/sdcard/Movies"));
assert(!strcmp(opts->record_filename, "file"));
assert(opts->record_format == SC_RECORD_FORMAT_MKV);
assert(opts->render_expired_frames);
assert(!strcmp(opts->serial, "0123456789abcdef"));
assert(opts->show_touches);
assert(opts->turn_screen_off);

View File

@ -146,18 +146,14 @@ static void test_serialize_inject_scroll_event(void) {
static void test_serialize_back_or_screen_on(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
.back_or_screen_on = {
.action = AKEY_EVENT_ACTION_UP,
},
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf);
assert(size == 2);
assert(size == 1);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
0x01, // AKEY_EVENT_ACTION_UP
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@ -177,9 +173,9 @@ static void test_serialize_expand_notification_panel(void) {
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_expand_settings_panel(void) {
static void test_serialize_collapse_notification_panel(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
@ -187,22 +183,7 @@ static void test_serialize_expand_settings_panel(void) {
assert(size == 1);
const unsigned char expected[] = {
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,
CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL,
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
@ -289,8 +270,7 @@ int main(int argc, char *argv[]) {
test_serialize_inject_scroll_event();
test_serialize_back_or_screen_on();
test_serialize_expand_notification_panel();
test_serialize_expand_settings_panel();
test_serialize_collapse_panels();
test_serialize_collapse_notification_panel();
test_serialize_get_clipboard();
test_serialize_set_clipboard();
test_serialize_set_screen_power_mode();

View File

@ -287,18 +287,6 @@ static void test_parse_integer_with_suffix(void) {
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[]) {
(void) argc;
(void) argv;
@ -316,6 +304,5 @@ int main(int argc, char *argv[]) {
test_parse_integer();
test_parse_integers();
test_parse_integer_with_suffix();
test_strlist_contains();
return 0;
}

View File

@ -2,11 +2,11 @@
[binaries]
name = 'mingw'
c = 'i686-w64-mingw32-gcc'
cpp = 'i686-w64-mingw32-g++'
ar = 'i686-w64-mingw32-ar'
strip = 'i686-w64-mingw32-strip'
pkgconfig = 'i686-w64-mingw32-pkg-config'
c = '/usr/bin/i686-w64-mingw32-gcc'
cpp = '/usr/bin/i686-w64-mingw32-g++'
ar = '/usr/bin/i686-w64-mingw32-ar'
strip = '/usr/bin/i686-w64-mingw32-strip'
pkgconfig = '/usr/bin/i686-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'

View File

@ -2,11 +2,11 @@
[binaries]
name = 'mingw'
c = 'x86_64-w64-mingw32-gcc'
cpp = 'x86_64-w64-mingw32-g++'
ar = 'x86_64-w64-mingw32-ar'
strip = 'x86_64-w64-mingw32-strip'
pkgconfig = 'x86_64-w64-mingw32-pkg-config'
c = '/usr/bin/x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-g++'
ar = '/usr/bin/x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
[host_machine]
system = 'windows'

View File

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

View File

@ -4,7 +4,6 @@ project('scrcpy', 'c',
default_options: [
'c_std=c11',
'warning_level=2',
'b_ndebug=if-release',
])
if get_option('compile_app')

View File

@ -3,5 +3,6 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
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('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_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")')

View File

@ -3,10 +3,6 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.File;
import java.io.IOException;
@ -19,123 +15,23 @@ public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable {
public static final Creator<Config> CREATOR = new Creator<Config>() {
@Override
public Config createFromParcel(Parcel in) {
return new Config(in);
}
@Override
public Config[] newArray(int size) {
return new Config[size];
}
};
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
private static final int FLAG_POWER_OFF_SCREEN = 4;
private int displayId;
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
private int restoreStayOn = -1;
private boolean disableShowTouches;
private boolean restoreNormalPowerMode;
private boolean powerOffScreen;
public Config() {
// Default constructor, the fields are initialized by CleanUp.configure()
}
protected Config(Parcel in) {
displayId = in.readInt();
restoreStayOn = in.readInt();
byte options = in.readByte();
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(displayId);
dest.writeInt(restoreStayOn);
byte options = 0;
if (disableShowTouches) {
options |= FLAG_DISABLE_SHOW_TOUCHES;
}
if (restoreNormalPowerMode) {
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
}
if (powerOffScreen) {
options |= FLAG_POWER_OFF_SCREEN;
}
dest.writeByte(options);
}
private boolean hasWork() {
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
}
@Override
public int describeContents() {
return 0;
}
byte[] serialize() {
Parcel parcel = Parcel.obtain();
writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}
static Config deserialize(byte[] bytes) {
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
return CREATOR.createFromParcel(parcel);
}
static Config fromBase64(String base64) {
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
return deserialize(bytes);
}
String toBase64() {
byte[] bytes = serialize();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
}
private CleanUp() {
// not instantiable
}
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
throws IOException {
Config config = new Config();
config.displayId = displayId;
config.disableShowTouches = disableShowTouches;
config.restoreStayOn = restoreStayOn;
config.restoreNormalPowerMode = restoreNormalPowerMode;
config.powerOffScreen = powerOffScreen;
if (config.hasWork()) {
startProcess(config);
public static void configure(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
boolean needProcess = disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode;
if (needProcess) {
startProcess(disableShowTouches, restoreStayOn, restoreNormalPowerMode);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(Config config) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
private static void startProcess(boolean disableShowTouches, int restoreStayOn, boolean restoreNormalPowerMode) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(disableShowTouches), String.valueOf(
restoreStayOn), String.valueOf(restoreNormalPowerMode)};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
@ -162,28 +58,27 @@ public final class CleanUp {
Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]);
boolean disableShowTouches = Boolean.parseBoolean(args[0]);
int restoreStayOn = Integer.parseInt(args[1]);
boolean restoreNormalPowerMode = Boolean.parseBoolean(args[2]);
if (config.disableShowTouches || config.restoreStayOn != -1) {
if (disableShowTouches || restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
try (ContentProvider settings = serviceManager.getActivityManager().createSettingsProvider()) {
if (config.disableShowTouches) {
if (disableShowTouches) {
Ln.i("Disabling \"show touches\"");
settings.putValue(ContentProvider.TABLE_SYSTEM, "show_touches", "0");
}
if (config.restoreStayOn != -1) {
if (restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
settings.putValue(ContentProvider.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(restoreStayOn));
}
}
}
if (Device.isScreenOn()) {
if (config.powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(config.displayId);
} else if (config.restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
if (restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
if (Device.isScreenOn()) {
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
}

View File

@ -11,12 +11,11 @@ public final class ControlMessage {
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
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_SETTINGS_PANEL = 6;
public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11;
public static final int TYPE_COLLAPSE_NOTIFICATION_PANEL = 6;
public static final int TYPE_GET_CLIPBOARD = 7;
public static final int TYPE_SET_CLIPBOARD = 8;
public static final int TYPE_SET_SCREEN_POWER_MODE = 9;
public static final int TYPE_ROTATE_DEVICE = 10;
private int type;
private String text;
@ -72,13 +71,6 @@ public final class ControlMessage {
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) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;

View File

@ -11,7 +11,6 @@ public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
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_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
@ -67,18 +66,15 @@ public class ControlMessageReader {
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOnEvent();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
case ControlMessage.TYPE_GET_CLIPBOARD:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
@ -154,14 +150,6 @@ public class ControlMessageReader {
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() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;

View File

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

View File

@ -25,9 +25,6 @@ public final class Device {
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 LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
public interface RotationListener {
@ -156,16 +153,12 @@ public final class Device {
return Build.MODEL;
}
public static boolean supportsInputEvents(int displayId) {
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public boolean supportsInputEvents() {
return supportsInputEvents;
}
public static boolean injectEvent(InputEvent inputEvent, int displayId) {
if (!supportsInputEvents(displayId)) {
public boolean injectEvent(InputEvent inputEvent, int mode) {
if (!supportsInputEvents()) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
@ -173,30 +166,22 @@ public final class Device {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, mode);
}
public boolean injectEvent(InputEvent event) {
return injectEvent(event, displayId);
}
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event, displayId);
return injectEvent(event, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState) {
return injectKeyEvent(action, keyCode, repeat, metaState, displayId);
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event);
}
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 boolean injectKeycode(int keyCode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0);
}
public static boolean isScreenOn() {
@ -215,10 +200,6 @@ public final class Device {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
}
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
}
public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
}
@ -268,13 +249,6 @@ public final class Device {
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).
*/

View File

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

View File

@ -82,12 +82,6 @@ public final class ScreenInfo {
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
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();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {

View File

@ -50,7 +50,7 @@ public final class Server {
}
}
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
CleanUp.configure(mustDisableShowTouchesOnCleanUp, restoreStayOn, true);
boolean tunnelForward = options.isTunnelForward();
@ -135,7 +135,7 @@ public final class Server {
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
final int expectedParameters = 16;
final int expectedParameters = 15;
if (args.length != expectedParameters) {
throw new IllegalArgumentException("Expecting " + expectedParameters + " parameters");
}
@ -185,9 +185,6 @@ public final class Server {
String encoderName = "-".equals(args[14]) ? null : args[14];
options.setEncoderName(encoderName);
boolean powerOffScreenOnClose = Boolean.parseBoolean(args[15]);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
return options;
}
@ -233,7 +230,7 @@ public final class Server {
if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
Ln.e(" scrcpy --encoder-name '" + encoder.getName() + "'");
}
}
}

View File

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

View File

@ -11,8 +11,6 @@ public class StatusBarManager {
private final IInterface manager;
private Method expandNotificationsPanelMethod;
private Method expandSettingsPanelMethod;
private boolean expandSettingsPanelMethodNewVersion = true;
private Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) {
@ -26,20 +24,6 @@ public class StatusBarManager {
return expandNotificationsPanelMethod;
}
private Method getExpandSettingsPanel() throws NoSuchMethodException {
if (expandSettingsPanelMethod == null) {
try {
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
} catch (NoSuchMethodException e) {
// old version
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
expandSettingsPanelMethodNewVersion = false;
}
}
return expandSettingsPanelMethod;
}
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
if (collapsePanelsMethod == null) {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
@ -56,21 +40,6 @@ public class StatusBarManager {
}
}
public void expandSettingsPanel() {
try {
Method method = getExpandSettingsPanel();
if (expandSettingsPanelMethodNewVersion) {
// new version
method.invoke(manager, (Object) null);
} else {
// old version
method.invoke(manager);
}
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void collapsePanels() {
try {
Method method = getCollapsePanelsMethod();

View File

@ -154,7 +154,6 @@ public class ControlMessageReaderTest {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
dos.writeByte(KeyEvent.ACTION_UP);
byte[] packet = bos.toByteArray();
@ -162,7 +161,6 @@ public class ControlMessageReaderTest {
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
}
@Test
@ -182,35 +180,19 @@ public class ControlMessageReaderTest {
}
@Test
public void testParseExpandSettingsPanelEvent() throws IOException {
public void testParseCollapseNotificationPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
}
@Test
public void testParseCollapsePanelsEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL, event.getType());
}
@Test