Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59cd1f55ad | ||
|
|
0cd902bd07 | ||
|
|
860569641f | ||
|
|
a963d89a64 | ||
|
|
195073d628 | ||
|
|
9d3bf09474 | ||
|
|
b2e67b5a60 | ||
|
|
066f4f5e88 | ||
|
|
c8eb5cc6e3 | ||
|
|
49582e4a2e | ||
|
|
99c6a76eb5 | ||
|
|
c9a5611382 | ||
|
|
7309a573dc | ||
|
|
8ede4b3f58 | ||
|
|
cec5bcbe0b | ||
|
|
6d8f0a247f | ||
|
|
bb6ac2b084 | ||
|
|
eb971390ed | ||
|
|
f27403e27f | ||
|
|
f82aab2057 | ||
|
|
71327e82cb | ||
|
|
fea3f29ffd | ||
|
|
fb07f4af55 | ||
|
|
1116502704 | ||
|
|
321bf79a0b | ||
|
|
85f6f32b9e | ||
|
|
7bb17e1abc | ||
|
|
adaa88952d | ||
|
|
8bae1f6b7f | ||
|
|
e3da97a80f | ||
|
|
b9c3f65fd8 | ||
|
|
d0739911a3 | ||
|
|
964b6d2243 | ||
|
|
8cc057c8f1 | ||
|
|
edee69d637 | ||
|
|
8ef4c044fa | ||
|
|
c23c38f99d | ||
|
|
65c4f487b3 | ||
|
|
c6d7f5ee96 |
57
README.md
57
README.md
@@ -198,6 +198,7 @@ If `--max-size` is also specified, resizing is applied after cropping.
|
|||||||
To lock the orientation of the mirroring:
|
To lock the orientation of the mirroring:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
scrcpy --lock-video-orientation # initial (current) orientation
|
||||||
scrcpy --lock-video-orientation 0 # natural orientation
|
scrcpy --lock-video-orientation 0 # natural orientation
|
||||||
scrcpy --lock-video-orientation 1 # 90° counterclockwise
|
scrcpy --lock-video-orientation 1 # 90° counterclockwise
|
||||||
scrcpy --lock-video-orientation 2 # 180°
|
scrcpy --lock-video-orientation 2 # 180°
|
||||||
@@ -225,7 +226,9 @@ error will give the available encoders:
|
|||||||
scrcpy --encoder _
|
scrcpy --encoder _
|
||||||
```
|
```
|
||||||
|
|
||||||
### Recording
|
### Capture
|
||||||
|
|
||||||
|
#### Recording
|
||||||
|
|
||||||
It is possible to record the screen while mirroring:
|
It is possible to record the screen while mirroring:
|
||||||
|
|
||||||
@@ -249,6 +252,58 @@ variation] does not impact the recorded file.
|
|||||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||||
|
|
||||||
|
|
||||||
|
#### v4l2loopback
|
||||||
|
|
||||||
|
On Linux, it is possible to send the video stream to a v4l2 loopback device, so
|
||||||
|
that the Android device can be opened like a webcam by any v4l2-capable tool.
|
||||||
|
|
||||||
|
The module `v4l2loopback` must be installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install v4l2loopback-dkms
|
||||||
|
```
|
||||||
|
|
||||||
|
To create a v4l2 device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo modprobe v4l2loopback
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a new video device in `/dev/videoN`, where `N` is an integer
|
||||||
|
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||||
|
to create several devices or devices with specific IDs).
|
||||||
|
|
||||||
|
To list the enabled devices:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# requires v4l-utils package
|
||||||
|
v4l2-ctl --list-devices
|
||||||
|
|
||||||
|
# simple but might be sufficient
|
||||||
|
ls /dev/video*
|
||||||
|
```
|
||||||
|
|
||||||
|
To start scrcpy using a v4l2 sink:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --v4l2-sink=/dev/videoN
|
||||||
|
scrcpy --v4l2-sink=/dev/videoN -N # --no-display to disable mirroring window
|
||||||
|
```
|
||||||
|
|
||||||
|
(replace `N` by the device ID, check with `ls /dev/video*`)
|
||||||
|
|
||||||
|
Once enabled, you can open your video stream with a v4l2-capable tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffplay -i /dev/videoN
|
||||||
|
vlc v4l2:///dev/videoN # VLC might add some buffering delay
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, you could capture the video within [OBS].
|
||||||
|
|
||||||
|
[OBS]: https://obsproject.com/
|
||||||
|
|
||||||
|
|
||||||
### Connection
|
### Connection
|
||||||
|
|
||||||
#### Wireless
|
#### Wireless
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ else
|
|||||||
src += [ 'src/sys/unix/process.c' ]
|
src += [ 'src/sys/unix/process.c' ]
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
v4l2_support = host_machine.system() == 'linux'
|
||||||
|
if v4l2_support
|
||||||
|
src += [ 'src/v4l2_sink.c' ]
|
||||||
|
endif
|
||||||
|
|
||||||
check_functions = [
|
check_functions = [
|
||||||
'strdup'
|
'strdup'
|
||||||
]
|
]
|
||||||
@@ -49,6 +54,10 @@ if not get_option('crossbuild_windows')
|
|||||||
dependency('sdl2'),
|
dependency('sdl2'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if v4l2_support
|
||||||
|
dependencies += dependency('libavdevice')
|
||||||
|
endif
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
||||||
# cross-compile mingw32 build (from Linux to Windows)
|
# cross-compile mingw32 build (from Linux to Windows)
|
||||||
@@ -124,6 +133,9 @@ conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
|||||||
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
|
# select the debugger method ('old' for Android < 9, 'new' for Android >= 9)
|
||||||
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
|
conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new')
|
||||||
|
|
||||||
|
# enable V4L2 support (linux only)
|
||||||
|
conf.set('HAVE_V4L2', v4l2_support)
|
||||||
|
|
||||||
configure_file(configuration: conf, output: 'config.h')
|
configure_file(configuration: conf, output: 'config.h')
|
||||||
|
|
||||||
src_dir = include_directories('src')
|
src_dir = include_directories('src')
|
||||||
|
|||||||
18
app/scrcpy.1
18
app/scrcpy.1
@@ -83,10 +83,12 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
|
|||||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-lock\-video\-orientation " value
|
.BI "\-\-lock\-video\-orientation " [value]
|
||||||
Lock video orientation to \fIvalue\fR. Possible values are -1 (unlocked), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
|
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees otation counterclockwise.
|
||||||
|
|
||||||
Default is -1 (unlocked).
|
Default is "unlocked".
|
||||||
|
|
||||||
|
Passing the option without argument is equivalent to passing "initial".
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-max\-fps " value
|
.BI "\-\-max\-fps " value
|
||||||
@@ -184,8 +186,10 @@ Enable "show touches" on start, restore the initial value on exit.
|
|||||||
It only shows physical touches (not clicks from scrcpy).
|
It only shows physical touches (not clicks from scrcpy).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-v, \-\-version
|
.BI "\-\-v4l2-sink " /dev/videoN
|
||||||
Print the version of scrcpy.
|
Output to v4l2loopback device.
|
||||||
|
|
||||||
|
It requires to lock the video orientation (see --lock-video-orientation).
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-V, \-\-verbosity " value
|
.BI "\-V, \-\-verbosity " value
|
||||||
@@ -193,6 +197,10 @@ Set the log level ("debug", "info", "warn" or "error").
|
|||||||
|
|
||||||
Default is "info" for release builds, "debug" for debug builds.
|
Default is "info" for release builds, "debug" for debug builds.
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B \-v, \-\-version
|
||||||
|
Print the version of scrcpy.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-w, \-\-stay-awake
|
.B \-w, \-\-stay-awake
|
||||||
Keep the device on while scrcpy is running, when the device is plugged in.
|
Keep the device on while scrcpy is running, when the device is plugged in.
|
||||||
|
|||||||
@@ -79,12 +79,15 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" This is a workaround for some devices not behaving as\n"
|
" This is a workaround for some devices not behaving as\n"
|
||||||
" expected when setting the device clipboard programmatically.\n"
|
" expected when setting the device clipboard programmatically.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --lock-video-orientation value\n"
|
" --lock-video-orientation [value]\n"
|
||||||
" Lock video orientation to value.\n"
|
" Lock video orientation to value.\n"
|
||||||
" Possible values are -1 (unlocked), 0, 1, 2 and 3.\n"
|
" Possible values are \"unlocked\", \"initial\" (locked to the\n"
|
||||||
|
" initial orientation), 0, 1, 2 and 3.\n"
|
||||||
" Natural device orientation is 0, and each increment adds a\n"
|
" Natural device orientation is 0, and each increment adds a\n"
|
||||||
" 90 degrees rotation counterclockwise.\n"
|
" 90 degrees rotation counterclockwise.\n"
|
||||||
" Default is -1 (unlocked).\n"
|
" Default is \"unlocked\".\n"
|
||||||
|
" Passing the option without argument is equivalent to passing\n"
|
||||||
|
" \"initial\".\n"
|
||||||
"\n"
|
"\n"
|
||||||
" --max-fps value\n"
|
" --max-fps value\n"
|
||||||
" Limit the frame rate of screen capture (officially supported\n"
|
" Limit the frame rate of screen capture (officially supported\n"
|
||||||
@@ -173,9 +176,13 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
" on exit.\n"
|
" on exit.\n"
|
||||||
" It only shows physical touches (not clicks from scrcpy).\n"
|
" It only shows physical touches (not clicks from scrcpy).\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -v, --version\n"
|
#ifdef HAVE_V4L2
|
||||||
" Print the version of scrcpy.\n"
|
" --v4l2-sink /dev/videoN\n"
|
||||||
|
" Output to v4l2loopback device.\n"
|
||||||
|
" It requires to lock the video orientation (see\n"
|
||||||
|
" --lock-video-orientation).\n"
|
||||||
"\n"
|
"\n"
|
||||||
|
#endif
|
||||||
" -V, --verbosity value\n"
|
" -V, --verbosity value\n"
|
||||||
" Set the log level (debug, info, warn or error).\n"
|
" Set the log level (debug, info, warn or error).\n"
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
@@ -183,6 +190,9 @@ scrcpy_print_usage(const char *arg0) {
|
|||||||
#else
|
#else
|
||||||
" Default is info.\n"
|
" Default is info.\n"
|
||||||
#endif
|
#endif
|
||||||
|
"\n"
|
||||||
|
" -v, --version\n"
|
||||||
|
" Print the version of scrcpy.\n"
|
||||||
"\n"
|
"\n"
|
||||||
" -w, --stay-awake\n"
|
" -w, --stay-awake\n"
|
||||||
" Keep the device on while scrcpy is running, when the device\n"
|
" Keep the device on while scrcpy is running, when the device\n"
|
||||||
@@ -383,15 +393,27 @@ parse_max_fps(const char *s, uint16_t *max_fps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
parse_lock_video_orientation(const char *s, int8_t *lock_video_orientation) {
|
parse_lock_video_orientation(const char *s,
|
||||||
|
enum sc_lock_video_orientation *lock_mode) {
|
||||||
|
if (!s || !strcmp(s, "initial")) {
|
||||||
|
// Without argument, lock the initial orientation
|
||||||
|
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strcmp(s, "unlocked")) {
|
||||||
|
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
long value;
|
long value;
|
||||||
bool ok = parse_integer_arg(s, &value, false, -1, 3,
|
bool ok = parse_integer_arg(s, &value, false, 0, 3,
|
||||||
"lock video orientation");
|
"lock video orientation");
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*lock_video_orientation = (int8_t) value;
|
*lock_mode = (enum sc_lock_video_orientation) value;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,6 +683,7 @@ guess_record_format(const char *filename) {
|
|||||||
#define OPT_LEGACY_PASTE 1024
|
#define OPT_LEGACY_PASTE 1024
|
||||||
#define OPT_ENCODER_NAME 1025
|
#define OPT_ENCODER_NAME 1025
|
||||||
#define OPT_POWER_OFF_ON_CLOSE 1026
|
#define OPT_POWER_OFF_ON_CLOSE 1026
|
||||||
|
#define OPT_V4L2_SINK 1027
|
||||||
|
|
||||||
bool
|
bool
|
||||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
||||||
@@ -680,7 +703,7 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
{"fullscreen", no_argument, NULL, 'f'},
|
{"fullscreen", no_argument, NULL, 'f'},
|
||||||
{"help", no_argument, NULL, 'h'},
|
{"help", no_argument, NULL, 'h'},
|
||||||
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
|
{"legacy-paste", no_argument, NULL, OPT_LEGACY_PASTE},
|
||||||
{"lock-video-orientation", required_argument, NULL,
|
{"lock-video-orientation", optional_argument, NULL,
|
||||||
OPT_LOCK_VIDEO_ORIENTATION},
|
OPT_LOCK_VIDEO_ORIENTATION},
|
||||||
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
{"max-fps", required_argument, NULL, OPT_MAX_FPS},
|
||||||
{"max-size", required_argument, NULL, 'm'},
|
{"max-size", required_argument, NULL, 'm'},
|
||||||
@@ -702,6 +725,9 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
{"show-touches", no_argument, NULL, 't'},
|
{"show-touches", no_argument, NULL, 't'},
|
||||||
{"stay-awake", no_argument, NULL, 'w'},
|
{"stay-awake", no_argument, NULL, 'w'},
|
||||||
{"turn-screen-off", no_argument, NULL, 'S'},
|
{"turn-screen-off", no_argument, NULL, 'S'},
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
{"v4l2-sink", required_argument, NULL, OPT_V4L2_SINK},
|
||||||
|
#endif
|
||||||
{"verbosity", required_argument, NULL, 'V'},
|
{"verbosity", required_argument, NULL, 'V'},
|
||||||
{"version", no_argument, NULL, 'v'},
|
{"version", no_argument, NULL, 'v'},
|
||||||
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
{"window-title", required_argument, NULL, OPT_WINDOW_TITLE},
|
||||||
@@ -765,7 +791,8 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_LOCK_VIDEO_ORIENTATION:
|
case OPT_LOCK_VIDEO_ORIENTATION:
|
||||||
if (!parse_lock_video_orientation(optarg, &opts->lock_video_orientation)) {
|
if (!parse_lock_video_orientation(optarg,
|
||||||
|
&opts->lock_video_orientation)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -885,16 +912,36 @@ scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
|
|||||||
case OPT_POWER_OFF_ON_CLOSE:
|
case OPT_POWER_OFF_ON_CLOSE:
|
||||||
opts->power_off_on_close = true;
|
opts->power_off_on_close = true;
|
||||||
break;
|
break;
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
case OPT_V4L2_SINK:
|
||||||
|
opts->v4l2_device = optarg;
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
|
||||||
|
LOGE("-N/--no-display requires either screen recording (-r/--record)"
|
||||||
|
" or sink to v4l2loopback device (--v4l2-sink)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts->v4l2_device && opts->lock_video_orientation
|
||||||
|
== SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||||
|
LOGI("Video orientation is locked for v4l2 sink. "
|
||||||
|
"See --lock-video-orientation.");
|
||||||
|
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
|
||||||
|
}
|
||||||
|
#else
|
||||||
if (!opts->display && !opts->record_filename) {
|
if (!opts->display && !opts->record_filename) {
|
||||||
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
LOGE("-N/--no-display requires screen recording (-r/--record)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
int index = optind;
|
int index = optind;
|
||||||
if (index < argc) {
|
if (index < argc) {
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
|||||||
buffer_write32be(&buf[17],
|
buffer_write32be(&buf[17],
|
||||||
(uint32_t) msg->inject_scroll_event.vscroll);
|
(uint32_t) msg->inject_scroll_event.vscroll);
|
||||||
return 21;
|
return 21;
|
||||||
|
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
|
||||||
|
buf[1] = msg->inject_keycode.action;
|
||||||
|
return 2;
|
||||||
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
case CONTROL_MSG_TYPE_SET_CLIPBOARD: {
|
||||||
buf[1] = !!msg->set_clipboard.paste;
|
buf[1] = !!msg->set_clipboard.paste;
|
||||||
size_t len = write_string(msg->set_clipboard.text,
|
size_t len = write_string(msg->set_clipboard.text,
|
||||||
@@ -77,7 +80,6 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
|
|||||||
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
||||||
buf[1] = msg->set_screen_power_mode.mode;
|
buf[1] = msg->set_screen_power_mode.mode;
|
||||||
return 2;
|
return 2;
|
||||||
case CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
|
|
||||||
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
case CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ struct control_msg {
|
|||||||
int32_t hscroll;
|
int32_t hscroll;
|
||||||
int32_t vscroll;
|
int32_t vscroll;
|
||||||
} inject_scroll_event;
|
} 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 {
|
struct {
|
||||||
char *text; // owned, to be freed by free()
|
char *text; // owned, to be freed by free()
|
||||||
bool paste;
|
bool paste;
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ decoder_open(struct decoder *decoder, const AVCodec *codec) {
|
|||||||
av_frame_free(&decoder->frame);
|
av_frame_free(&decoder->frame);
|
||||||
avcodec_close(decoder->codec_ctx);
|
avcodec_close(decoder->codec_ctx);
|
||||||
avcodec_free_context(&decoder->codec_ctx);
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -98,8 +99,8 @@ decoder_push(struct decoder *decoder, const AVPacket *packet) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ret;
|
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
|
||||||
if ((ret = avcodec_send_packet(decoder->codec_ctx, packet)) < 0) {
|
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
||||||
LOGE("Could not send video packet: %d", ret);
|
LOGE("Could not send video packet: %d", ret);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
#define DECODER_MAX_SINKS 1
|
#define DECODER_MAX_SINKS 2
|
||||||
|
|
||||||
struct decoder {
|
struct decoder {
|
||||||
struct sc_packet_sink packet_sink; // packet sink trait
|
struct sc_packet_sink packet_sink; // packet sink trait
|
||||||
|
|||||||
@@ -146,13 +146,25 @@ action_cut(struct controller *controller, int actions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// turn the screen on if it was off, press BACK otherwise
|
// turn the screen on if it was off, press BACK otherwise
|
||||||
|
// If the screen is off, it is turned on only on ACTION_DOWN
|
||||||
static void
|
static void
|
||||||
press_back_or_turn_screen_on(struct controller *controller) {
|
press_back_or_turn_screen_on(struct controller *controller, int actions) {
|
||||||
struct control_msg msg;
|
struct control_msg msg;
|
||||||
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
||||||
|
|
||||||
if (!controller_push_msg(controller, &msg)) {
|
if (actions & ACTION_DOWN) {
|
||||||
LOGW("Could not request 'press back or turn screen on'");
|
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'");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,13 +658,23 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool down = event->type == SDL_MOUSEBUTTONDOWN;
|
bool down = event->type == SDL_MOUSEBUTTONDOWN;
|
||||||
if (!im->forward_all_clicks && down) {
|
if (!im->forward_all_clicks) {
|
||||||
|
int action = down ? ACTION_DOWN : ACTION_UP;
|
||||||
|
|
||||||
|
if (control && event->button == SDL_BUTTON_X1) {
|
||||||
|
action_app_switch(im->controller, action);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (control && event->button == SDL_BUTTON_X2 && down) {
|
||||||
|
expand_notification_panel(im->controller);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (control && event->button == SDL_BUTTON_RIGHT) {
|
if (control && event->button == SDL_BUTTON_RIGHT) {
|
||||||
press_back_or_turn_screen_on(im->controller);
|
press_back_or_turn_screen_on(im->controller, action);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
if (control && event->button == SDL_BUTTON_MIDDLE) {
|
||||||
action_home(im->controller, ACTION_DOWN | ACTION_UP);
|
action_home(im->controller, action);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +687,9 @@ input_manager_process_mouse_button(struct input_manager *im,
|
|||||||
bool outside = x < r->x || x >= r->x + r->w
|
bool outside = x < r->x || x >= r->x + r->w
|
||||||
|| y < r->y || y >= r->y + r->h;
|
|| y < r->y || y >= r->y + r->h;
|
||||||
if (outside) {
|
if (outside) {
|
||||||
screen_resize_to_fit(im->screen);
|
if (down) {
|
||||||
|
screen_resize_to_fit(im->screen);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
# include <libavdevice/avdevice.h>
|
||||||
|
#endif
|
||||||
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
|
#define SDL_MAIN_HANDLED // avoid link error on Linux Windows Subsystem
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
|
|
||||||
@@ -28,6 +31,11 @@ print_version(void) {
|
|||||||
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
|
fprintf(stderr, " - libavutil %d.%d.%d\n", LIBAVUTIL_VERSION_MAJOR,
|
||||||
LIBAVUTIL_VERSION_MINOR,
|
LIBAVUTIL_VERSION_MINOR,
|
||||||
LIBAVUTIL_VERSION_MICRO);
|
LIBAVUTIL_VERSION_MICRO);
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
fprintf(stderr, " - libavdevice %d.%d.%d\n", LIBAVDEVICE_VERSION_MAJOR,
|
||||||
|
LIBAVDEVICE_VERSION_MINOR,
|
||||||
|
LIBAVDEVICE_VERSION_MICRO);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static SDL_LogPriority
|
static SDL_LogPriority
|
||||||
@@ -90,6 +98,12 @@ main(int argc, char *argv[]) {
|
|||||||
av_register_all();
|
av_register_all();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
if (args.opts.v4l2_device) {
|
||||||
|
avdevice_register_all();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (avformat_network_init()) {
|
if (avformat_network_init()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <libavutil/time.h>
|
#include <libavutil/time.h>
|
||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
#include "util/str_util.h"
|
||||||
|
|
||||||
/** Downcast packet_sink to recorder */
|
/** Downcast packet_sink to recorder */
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink)
|
#define DOWNCAST(SINK) container_of(SINK, struct recorder, packet_sink)
|
||||||
@@ -22,8 +23,8 @@ find_muxer(const char *name) {
|
|||||||
#else
|
#else
|
||||||
oformat = av_oformat_next(oformat);
|
oformat = av_oformat_next(oformat);
|
||||||
#endif
|
#endif
|
||||||
// until null or with name "mp4"
|
// until null or containing the requested name
|
||||||
} while (oformat && strcmp(oformat->name, name));
|
} while (oformat && !strlist_contains(oformat->name, ',', name));
|
||||||
return oformat;
|
return oformat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,18 +219,36 @@ run_recorder(void *data) {
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
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);
|
const char *format_name = recorder_get_format_name(recorder->format);
|
||||||
assert(format_name);
|
assert(format_name);
|
||||||
const AVOutputFormat *format = find_muxer(format_name);
|
const AVOutputFormat *format = find_muxer(format_name);
|
||||||
if (!format) {
|
if (!format) {
|
||||||
LOGE("Could not find muxer");
|
LOGE("Could not find muxer");
|
||||||
return false;
|
goto error_cond_destroy;
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder->ctx = avformat_alloc_context();
|
recorder->ctx = avformat_alloc_context();
|
||||||
if (!recorder->ctx) {
|
if (!recorder->ctx) {
|
||||||
LOGE("Could not allocate output context");
|
LOGE("Could not allocate output context");
|
||||||
return false;
|
goto error_cond_destroy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||||
@@ -243,8 +262,7 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
|||||||
|
|
||||||
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
|
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
|
||||||
if (!ostream) {
|
if (!ostream) {
|
||||||
avformat_free_context(recorder->ctx);
|
goto error_avformat_free_context;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
@@ -258,22 +276,31 @@ recorder_open(struct recorder *recorder, const AVCodec *input_codec) {
|
|||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGE("Failed to open output file: %s", recorder->filename);
|
LOGE("Failed to open output file: %s", recorder->filename);
|
||||||
// ostream will be cleaned up during context cleaning
|
// ostream will be cleaned up during context cleaning
|
||||||
avformat_free_context(recorder->ctx);
|
goto error_avformat_free_context;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGD("Starting recorder thread");
|
LOGD("Starting recorder thread");
|
||||||
bool ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
|
ok = sc_thread_create(&recorder->thread, run_recorder, "recorder",
|
||||||
recorder);
|
recorder);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGC("Could not start recorder thread");
|
LOGC("Could not start recorder thread");
|
||||||
avformat_free_context(recorder->ctx);
|
goto error_avio_close;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
|
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
error_avio_close:
|
||||||
|
avio_close(recorder->ctx->pb);
|
||||||
|
error_avformat_free_context:
|
||||||
|
avformat_free_context(recorder->ctx);
|
||||||
|
error_cond_destroy:
|
||||||
|
sc_cond_destroy(&recorder->queue_cond);
|
||||||
|
error_mutex_destroy:
|
||||||
|
sc_mutex_destroy(&recorder->mutex);
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -287,6 +314,8 @@ recorder_close(struct recorder *recorder) {
|
|||||||
|
|
||||||
avio_close(recorder->ctx->pb);
|
avio_close(recorder->ctx->pb);
|
||||||
avformat_free_context(recorder->ctx);
|
avformat_free_context(recorder->ctx);
|
||||||
|
sc_cond_destroy(&recorder->queue_cond);
|
||||||
|
sc_mutex_destroy(&recorder->mutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -343,28 +372,8 @@ recorder_init(struct recorder *recorder,
|
|||||||
return false;
|
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->format = format;
|
||||||
recorder->declared_frame_size = declared_frame_size;
|
recorder->declared_frame_size = declared_frame_size;
|
||||||
recorder->header_written = false;
|
|
||||||
recorder->previous = NULL;
|
|
||||||
|
|
||||||
static const struct sc_packet_sink_ops ops = {
|
static const struct sc_packet_sink_ops ops = {
|
||||||
.open = recorder_packet_sink_open,
|
.open = recorder_packet_sink_open,
|
||||||
@@ -379,7 +388,5 @@ recorder_init(struct recorder *recorder,
|
|||||||
|
|
||||||
void
|
void
|
||||||
recorder_destroy(struct recorder *recorder) {
|
recorder_destroy(struct recorder *recorder) {
|
||||||
sc_cond_destroy(&recorder->queue_cond);
|
|
||||||
sc_mutex_destroy(&recorder->mutex);
|
|
||||||
free(recorder->filename);
|
free(recorder->filename);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
#include "tiny_xpm.h"
|
#include "tiny_xpm.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
# include "v4l2_sink.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
static struct server server;
|
static struct server server;
|
||||||
static struct screen screen;
|
static struct screen screen;
|
||||||
@@ -34,6 +37,9 @@ static struct fps_counter fps_counter;
|
|||||||
static struct stream stream;
|
static struct stream stream;
|
||||||
static struct decoder decoder;
|
static struct decoder decoder;
|
||||||
static struct recorder recorder;
|
static struct recorder recorder;
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
static struct sc_v4l2_sink v4l2_sink;
|
||||||
|
#endif
|
||||||
static struct controller controller;
|
static struct controller controller;
|
||||||
static struct file_handler file_handler;
|
static struct file_handler file_handler;
|
||||||
|
|
||||||
@@ -126,30 +132,6 @@ sdl_init_and_configure(bool display, const char *render_driver,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
|
||||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
|
||||||
// On Windows and MacOS, resizing blocks the event loop, so resizing events are
|
|
||||||
// not triggered. As a workaround, handle them in an event handler.
|
|
||||||
//
|
|
||||||
// <https://bugzilla.libsdl.org/show_bug.cgi?id=2077>
|
|
||||||
// <https://stackoverflow.com/a/40693139/1987178>
|
|
||||||
static int
|
|
||||||
event_watcher(void *data, SDL_Event *event) {
|
|
||||||
(void) data;
|
|
||||||
if (event->type == SDL_WINDOWEVENT
|
|
||||||
&& event->window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
||||||
// In practice, it seems to always be called from the same thread in
|
|
||||||
// that specific case. Anyway, it's just a workaround.
|
|
||||||
screen_render(&screen, true);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
is_apk(const char *file) {
|
is_apk(const char *file) {
|
||||||
const char *ext = strrchr(file, '.');
|
const char *ext = strrchr(file, '.');
|
||||||
@@ -189,7 +171,7 @@ handle_event(SDL_Event *event, const struct scrcpy_options *options) {
|
|||||||
action = ACTION_PUSH_FILE;
|
action = ACTION_PUSH_FILE;
|
||||||
}
|
}
|
||||||
file_handler_request(&file_handler, action, file);
|
file_handler_request(&file_handler, action, file);
|
||||||
break;
|
goto end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +189,6 @@ end:
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
event_loop(const struct scrcpy_options *options) {
|
event_loop(const struct scrcpy_options *options) {
|
||||||
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
|
||||||
if (options->display) {
|
|
||||||
SDL_AddEventWatch(event_watcher, NULL);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
SDL_Event event;
|
SDL_Event event;
|
||||||
while (SDL_WaitEvent(&event)) {
|
while (SDL_WaitEvent(&event)) {
|
||||||
enum event_result result = handle_event(&event, options);
|
enum event_result result = handle_event(&event, options);
|
||||||
@@ -276,6 +253,9 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
bool fps_counter_initialized = false;
|
bool fps_counter_initialized = false;
|
||||||
bool file_handler_initialized = false;
|
bool file_handler_initialized = false;
|
||||||
bool recorder_initialized = false;
|
bool recorder_initialized = false;
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
bool v4l2_sink_initialized = false;
|
||||||
|
#endif
|
||||||
bool stream_started = false;
|
bool stream_started = false;
|
||||||
bool controller_initialized = false;
|
bool controller_initialized = false;
|
||||||
bool controller_started = false;
|
bool controller_started = false;
|
||||||
@@ -324,7 +304,6 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct decoder *dec = NULL;
|
|
||||||
if (options->display) {
|
if (options->display) {
|
||||||
if (!fps_counter_init(&fps_counter)) {
|
if (!fps_counter_init(&fps_counter)) {
|
||||||
goto end;
|
goto end;
|
||||||
@@ -338,7 +317,14 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
}
|
}
|
||||||
file_handler_initialized = true;
|
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);
|
decoder_init(&decoder);
|
||||||
dec = &decoder;
|
dec = &decoder;
|
||||||
}
|
}
|
||||||
@@ -394,6 +380,7 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
.window_borderless = options->window_borderless,
|
.window_borderless = options->window_borderless,
|
||||||
.rotation = options->rotation,
|
.rotation = options->rotation,
|
||||||
.mipmaps = options->mipmaps,
|
.mipmaps = options->mipmaps,
|
||||||
|
.fullscreen = options->fullscreen,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!screen_init(&screen, &fps_counter, &screen_params)) {
|
if (!screen_init(&screen, &fps_counter, &screen_params)) {
|
||||||
@@ -412,12 +399,20 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
LOGW("Could not request 'set screen power mode'");
|
LOGW("Could not request 'set screen power mode'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->fullscreen) {
|
|
||||||
screen_switch_fullscreen(&screen);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
if (options->v4l2_device) {
|
||||||
|
if (!sc_v4l2_sink_init(&v4l2_sink, options->v4l2_device, frame_size)) {
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder_add_sink(&decoder, &v4l2_sink.frame_sink);
|
||||||
|
|
||||||
|
v4l2_sink_initialized = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// now we consumed the header values, the socket receives the video stream
|
// now we consumed the header values, the socket receives the video stream
|
||||||
// start the stream
|
// start the stream
|
||||||
if (!stream_start(&stream)) {
|
if (!stream_start(&stream)) {
|
||||||
@@ -430,6 +425,10 @@ scrcpy(const struct scrcpy_options *options) {
|
|||||||
ret = event_loop(options);
|
ret = event_loop(options);
|
||||||
LOGD("quit...");
|
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);
|
||||||
|
|
||||||
end:
|
end:
|
||||||
// The stream is not stopped explicitly, because it will stop by itself on
|
// The stream is not stopped explicitly, because it will stop by itself on
|
||||||
// end-of-stream
|
// end-of-stream
|
||||||
@@ -454,6 +453,12 @@ end:
|
|||||||
stream_join(&stream);
|
stream_join(&stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_V4L2
|
||||||
|
if (v4l2_sink_initialized) {
|
||||||
|
sc_v4l2_sink_destroy(&v4l2_sink);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Destroy the screen only after the stream is guaranteed to be finished,
|
// Destroy the screen only after the stream is guaranteed to be finished,
|
||||||
// because otherwise the screen could receive new frames after destruction
|
// because otherwise the screen could receive new frames after destruction
|
||||||
if (screen_initialized) {
|
if (screen_initialized) {
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ enum sc_record_format {
|
|||||||
SC_RECORD_FORMAT_MKV,
|
SC_RECORD_FORMAT_MKV,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum sc_lock_video_orientation {
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||||
|
// lock the current orientation when scrcpy starts
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_1,
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_2,
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_3,
|
||||||
|
};
|
||||||
|
|
||||||
#define SC_MAX_SHORTCUT_MODS 8
|
#define SC_MAX_SHORTCUT_MODS 8
|
||||||
|
|
||||||
enum sc_shortcut_mod {
|
enum sc_shortcut_mod {
|
||||||
@@ -52,6 +62,7 @@ struct scrcpy_options {
|
|||||||
const char *render_driver;
|
const char *render_driver;
|
||||||
const char *codec_options;
|
const char *codec_options;
|
||||||
const char *encoder_name;
|
const char *encoder_name;
|
||||||
|
const char *v4l2_device;
|
||||||
enum sc_log_level log_level;
|
enum sc_log_level log_level;
|
||||||
enum sc_record_format record_format;
|
enum sc_record_format record_format;
|
||||||
struct sc_port_range port_range;
|
struct sc_port_range port_range;
|
||||||
@@ -59,7 +70,7 @@ struct scrcpy_options {
|
|||||||
uint16_t max_size;
|
uint16_t max_size;
|
||||||
uint32_t bit_rate;
|
uint32_t bit_rate;
|
||||||
uint16_t max_fps;
|
uint16_t max_fps;
|
||||||
int8_t lock_video_orientation;
|
enum sc_lock_video_orientation lock_video_orientation;
|
||||||
uint8_t rotation;
|
uint8_t rotation;
|
||||||
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||||
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
|
||||||
@@ -93,6 +104,7 @@ struct scrcpy_options {
|
|||||||
.render_driver = NULL, \
|
.render_driver = NULL, \
|
||||||
.codec_options = NULL, \
|
.codec_options = NULL, \
|
||||||
.encoder_name = NULL, \
|
.encoder_name = NULL, \
|
||||||
|
.v4l2_device = NULL, \
|
||||||
.log_level = SC_LOG_LEVEL_INFO, \
|
.log_level = SC_LOG_LEVEL_INFO, \
|
||||||
.record_format = SC_RECORD_FORMAT_AUTO, \
|
.record_format = SC_RECORD_FORMAT_AUTO, \
|
||||||
.port_range = { \
|
.port_range = { \
|
||||||
@@ -106,7 +118,7 @@ struct scrcpy_options {
|
|||||||
.max_size = 0, \
|
.max_size = 0, \
|
||||||
.bit_rate = DEFAULT_BIT_RATE, \
|
.bit_rate = DEFAULT_BIT_RATE, \
|
||||||
.max_fps = 0, \
|
.max_fps = 0, \
|
||||||
.lock_video_orientation = -1, \
|
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, \
|
||||||
.rotation = 0, \
|
.rotation = 0, \
|
||||||
.window_x = SC_WINDOW_POSITION_UNDEFINED, \
|
.window_x = SC_WINDOW_POSITION_UNDEFINED, \
|
||||||
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
|
.window_y = SC_WINDOW_POSITION_UNDEFINED, \
|
||||||
|
|||||||
@@ -220,6 +220,29 @@ create_texture(struct screen *screen) {
|
|||||||
return texture;
|
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
|
static bool
|
||||||
screen_frame_sink_open(struct sc_frame_sink *sink) {
|
screen_frame_sink_open(struct sc_frame_sink *sink) {
|
||||||
struct screen *screen = DOWNCAST(sink);
|
struct screen *screen = DOWNCAST(sink);
|
||||||
@@ -402,6 +425,14 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
|
|||||||
|
|
||||||
screen_update_content_rect(screen);
|
screen_update_content_rect(screen);
|
||||||
|
|
||||||
|
if (params->fullscreen) {
|
||||||
|
screen_switch_fullscreen(screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONTINUOUS_RESIZING_WORKAROUND
|
||||||
|
SDL_AddEventWatch(event_watcher, screen);
|
||||||
|
#endif
|
||||||
|
|
||||||
static const struct sc_frame_sink_ops ops = {
|
static const struct sc_frame_sink_ops ops = {
|
||||||
.open = screen_frame_sink_open,
|
.open = screen_frame_sink_open,
|
||||||
.close = screen_frame_sink_close,
|
.close = screen_frame_sink_close,
|
||||||
@@ -417,11 +448,16 @@ screen_init(struct screen *screen, struct fps_counter *fps_counter,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static void
|
||||||
screen_show_window(struct screen *screen) {
|
screen_show_window(struct screen *screen) {
|
||||||
SDL_ShowWindow(screen->window);
|
SDL_ShowWindow(screen->window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
screen_hide_window(struct screen *screen) {
|
||||||
|
SDL_HideWindow(screen->window);
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
screen_destroy(struct screen *screen) {
|
screen_destroy(struct screen *screen) {
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "video_buffer.h"
|
#include "video_buffer.h"
|
||||||
|
|
||||||
struct video_buffer;
|
|
||||||
|
|
||||||
struct screen {
|
struct screen {
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
|
|
||||||
@@ -62,6 +60,8 @@ struct screen_params {
|
|||||||
|
|
||||||
uint8_t rotation;
|
uint8_t rotation;
|
||||||
bool mipmaps;
|
bool mipmaps;
|
||||||
|
|
||||||
|
bool fullscreen;
|
||||||
};
|
};
|
||||||
|
|
||||||
// initialize screen, create window, renderer and texture (window is hidden)
|
// initialize screen, create window, renderer and texture (window is hidden)
|
||||||
@@ -69,14 +69,17 @@ bool
|
|||||||
screen_init(struct screen *screen, struct fps_counter *fps_counter,
|
screen_init(struct screen *screen, struct fps_counter *fps_counter,
|
||||||
const struct screen_params *params);
|
const struct screen_params *params);
|
||||||
|
|
||||||
// show the window
|
|
||||||
void
|
|
||||||
screen_show_window(struct screen *screen);
|
|
||||||
|
|
||||||
// destroy window, renderer and texture (if any)
|
// destroy window, renderer and texture (if any)
|
||||||
void
|
void
|
||||||
screen_destroy(struct screen *screen);
|
screen_destroy(struct screen *screen);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
// render the texture to the renderer
|
// render the texture to the renderer
|
||||||
//
|
//
|
||||||
// Set the update_content_rect flag if the window or content size may have
|
// Set the update_content_rect flag if the window or content size may have
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frame sink trait.
|
* Frame sink trait.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
|
typedef struct AVCodec AVCodec;
|
||||||
|
typedef struct AVPacket AVPacket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet sink trait.
|
* Packet sink trait.
|
||||||
|
|||||||
@@ -140,6 +140,24 @@ parse_integer_with_suffix(const char *s, long *out) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
strlist_contains(const char *list, char sep, const char *s) {
|
||||||
|
char *p;
|
||||||
|
do {
|
||||||
|
p = strchr(list, sep);
|
||||||
|
|
||||||
|
size_t token_len = p ? (size_t) (p - list) : strlen(list);
|
||||||
|
if (!strncmp(list, s, token_len)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p) {
|
||||||
|
list = p + 1;
|
||||||
|
}
|
||||||
|
} while (p);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t
|
size_t
|
||||||
utf8_truncation_index(const char *utf8, size_t max_len) {
|
utf8_truncation_index(const char *utf8, size_t max_len) {
|
||||||
size_t len = strlen(utf8);
|
size_t len = strlen(utf8);
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ parse_integers(const char *s, const char sep, size_t max_items, long *out);
|
|||||||
bool
|
bool
|
||||||
parse_integer_with_suffix(const char *s, long *out);
|
parse_integer_with_suffix(const char *s, long *out);
|
||||||
|
|
||||||
|
// search s in the list separated by sep
|
||||||
|
// for example, strlist_contains("a,bc,def", ',', "bc") returns true
|
||||||
|
bool
|
||||||
|
strlist_contains(const char *list, char sep, const char *s);
|
||||||
|
|
||||||
// return the index to truncate a UTF-8 string at a valid position
|
// return the index to truncate a UTF-8 string at a valid position
|
||||||
size_t
|
size_t
|
||||||
utf8_truncation_index(const char *utf8, size_t max_len);
|
utf8_truncation_index(const char *utf8, size_t max_len);
|
||||||
|
|||||||
341
app/src/v4l2_sink.c
Normal file
341
app/src/v4l2_sink.c
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
#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);
|
||||||
|
if (!ok) {
|
||||||
|
LOGW("Could not send packet to v4l2 sink");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
av_packet_unref(packet);
|
||||||
|
} 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);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not send frame to v4l2 sink");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("V4l2 thread ended");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
||||||
|
bool ok = video_buffer_init(&vs->vb);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_mutex_init(&vs->mutex);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not create mutex");
|
||||||
|
goto error_video_buffer_destroy;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&vs->cond);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not create cond");
|
||||||
|
goto error_mutex_destroy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME
|
||||||
|
const AVOutputFormat *format = find_muxer("video4linux2,v4l2");
|
||||||
|
if (!format) {
|
||||||
|
LOGE("Could not find v4l2 muxer");
|
||||||
|
goto error_cond_destroy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVCodec *encoder = avcodec_find_encoder(AV_CODEC_ID_RAWVIDEO);
|
||||||
|
if (!encoder) {
|
||||||
|
LOGE("Raw video encoder not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->format_ctx = avformat_alloc_context();
|
||||||
|
if (!vs->format_ctx) {
|
||||||
|
LOGE("Could not allocate v4l2 output context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||||
|
// returns (on purpose) a pointer-to-const, but AVFormatContext.oformat
|
||||||
|
// still expects a pointer-to-non-const (it has not be updated accordingly)
|
||||||
|
// <https://github.com/FFmpeg/FFmpeg/commit/0694d8702421e7aff1340038559c438b61bb30dd>
|
||||||
|
vs->format_ctx->oformat = (AVOutputFormat *) format;
|
||||||
|
vs->format_ctx->url = strdup(vs->device_name);
|
||||||
|
if (!vs->format_ctx->url) {
|
||||||
|
LOGE("Could not strdup v4l2 device name");
|
||||||
|
goto error_avformat_free_context;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVStream *ostream = avformat_new_stream(vs->format_ctx, encoder);
|
||||||
|
if (!ostream) {
|
||||||
|
LOGE("Could not allocate new v4l2 stream");
|
||||||
|
goto error_avformat_free_context;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
|
ostream->codecpar->codec_id = encoder->id;
|
||||||
|
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||||
|
ostream->codecpar->width = vs->frame_size.width;
|
||||||
|
ostream->codecpar->height = vs->frame_size.height;
|
||||||
|
|
||||||
|
int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGE("Failed to open output device: %s", vs->device_name);
|
||||||
|
// ostream will be cleaned up during context cleaning
|
||||||
|
goto error_avformat_free_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->encoder_ctx = avcodec_alloc_context3(encoder);
|
||||||
|
if (!vs->encoder_ctx) {
|
||||||
|
LOGC("Could not allocate codec context for v4l2");
|
||||||
|
goto error_avio_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->encoder_ctx->width = vs->frame_size.width;
|
||||||
|
vs->encoder_ctx->height = vs->frame_size.height;
|
||||||
|
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
|
vs->encoder_ctx->time_base.num = 1;
|
||||||
|
vs->encoder_ctx->time_base.den = 1;
|
||||||
|
|
||||||
|
if (avcodec_open2(vs->encoder_ctx, encoder, NULL) < 0) {
|
||||||
|
LOGE("Could not open codec for v4l2");
|
||||||
|
goto error_avcodec_free_context;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->frame = av_frame_alloc();
|
||||||
|
if (!vs->frame) {
|
||||||
|
LOGE("Could not create v4l2 frame");
|
||||||
|
goto error_avcodec_close;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Starting v4l2 thread");
|
||||||
|
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
|
||||||
|
if (!ok) {
|
||||||
|
LOGC("Could not start v4l2 thread");
|
||||||
|
goto error_av_frame_free;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->header_written = false;
|
||||||
|
vs->stopped = false;
|
||||||
|
|
||||||
|
LOGI("v4l2 sink started to device: %s", vs->device_name);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error_av_frame_free:
|
||||||
|
av_frame_free(&vs->frame);
|
||||||
|
error_avcodec_close:
|
||||||
|
avcodec_close(vs->encoder_ctx);
|
||||||
|
error_avcodec_free_context:
|
||||||
|
avcodec_free_context(&vs->encoder_ctx);
|
||||||
|
error_avio_close:
|
||||||
|
avio_close(vs->format_ctx->pb);
|
||||||
|
error_avformat_free_context:
|
||||||
|
avformat_free_context(vs->format_ctx);
|
||||||
|
error_cond_destroy:
|
||||||
|
sc_cond_destroy(&vs->cond);
|
||||||
|
error_mutex_destroy:
|
||||||
|
sc_mutex_destroy(&vs->mutex);
|
||||||
|
error_video_buffer_destroy:
|
||||||
|
video_buffer_destroy(&vs->vb);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
||||||
|
sc_mutex_lock(&vs->mutex);
|
||||||
|
vs->stopped = true;
|
||||||
|
sc_cond_signal(&vs->cond);
|
||||||
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
|
||||||
|
sc_thread_join(&vs->thread, NULL);
|
||||||
|
|
||||||
|
av_frame_free(&vs->frame);
|
||||||
|
avcodec_close(vs->encoder_ctx);
|
||||||
|
avcodec_free_context(&vs->encoder_ctx);
|
||||||
|
avio_close(vs->format_ctx->pb);
|
||||||
|
avformat_free_context(vs->format_ctx);
|
||||||
|
sc_cond_destroy(&vs->cond);
|
||||||
|
sc_mutex_destroy(&vs->mutex);
|
||||||
|
video_buffer_destroy(&vs->vb);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||||
|
bool ok = video_buffer_push(&vs->vb, frame, NULL);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// signal possible change of vs->vb.pending_frame_consumed
|
||||||
|
sc_cond_signal(&vs->cond);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
|
||||||
|
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||||
|
return sc_v4l2_sink_open(vs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_v4l2_frame_sink_close(struct sc_frame_sink *sink) {
|
||||||
|
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||||
|
sc_v4l2_sink_close(vs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||||
|
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||||
|
return sc_v4l2_sink_push(vs, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
|
struct size frame_size) {
|
||||||
|
vs->device_name = strdup(device_name);
|
||||||
|
if (!vs->device_name) {
|
||||||
|
LOGE("Could not strdup v4l2 device name");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vs->frame_size = frame_size;
|
||||||
|
|
||||||
|
static const struct sc_frame_sink_ops ops = {
|
||||||
|
.open = sc_v4l2_frame_sink_open,
|
||||||
|
.close = sc_v4l2_frame_sink_close,
|
||||||
|
.push = sc_v4l2_frame_sink_push,
|
||||||
|
};
|
||||||
|
|
||||||
|
vs->frame_sink.ops = &ops;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs) {
|
||||||
|
free(vs->device_name);
|
||||||
|
}
|
||||||
39
app/src/v4l2_sink.h
Normal file
39
app/src/v4l2_sink.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef SC_V4L2_SINK_H
|
||||||
|
#define SC_V4L2_SINK_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include "coords.h"
|
||||||
|
#include "trait/frame_sink.h"
|
||||||
|
#include "video_buffer.h"
|
||||||
|
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
struct sc_v4l2_sink {
|
||||||
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
|
|
||||||
|
struct video_buffer vb;
|
||||||
|
AVFormatContext *format_ctx;
|
||||||
|
AVCodecContext *encoder_ctx;
|
||||||
|
|
||||||
|
char *device_name;
|
||||||
|
struct size frame_size;
|
||||||
|
|
||||||
|
sc_thread thread;
|
||||||
|
sc_mutex mutex;
|
||||||
|
sc_cond cond;
|
||||||
|
bool stopped;
|
||||||
|
bool header_written;
|
||||||
|
|
||||||
|
AVFrame *frame;
|
||||||
|
AVPacket packet;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
|
struct size frame_size);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -146,14 +146,18 @@ static void test_serialize_inject_scroll_event(void) {
|
|||||||
static void test_serialize_back_or_screen_on(void) {
|
static void test_serialize_back_or_screen_on(void) {
|
||||||
struct control_msg msg = {
|
struct control_msg msg = {
|
||||||
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
||||||
|
.back_or_screen_on = {
|
||||||
|
.action = AKEY_EVENT_ACTION_UP,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
unsigned char buf[CONTROL_MSG_MAX_SIZE];
|
||||||
size_t size = control_msg_serialize(&msg, buf);
|
size_t size = control_msg_serialize(&msg, buf);
|
||||||
assert(size == 1);
|
assert(size == 2);
|
||||||
|
|
||||||
const unsigned char expected[] = {
|
const unsigned char expected[] = {
|
||||||
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
||||||
|
0x01, // AKEY_EVENT_ACTION_UP
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,18 @@ static void test_parse_integer_with_suffix(void) {
|
|||||||
assert(!ok);
|
assert(!ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_strlist_contains(void) {
|
||||||
|
assert(strlist_contains("a,bc,def", ',', "bc"));
|
||||||
|
assert(!strlist_contains("a,bc,def", ',', "b"));
|
||||||
|
assert(strlist_contains("", ',', ""));
|
||||||
|
assert(strlist_contains("abc,", ',', ""));
|
||||||
|
assert(strlist_contains(",abc", ',', ""));
|
||||||
|
assert(strlist_contains("abc,,def", ',', ""));
|
||||||
|
assert(!strlist_contains("abc", ',', ""));
|
||||||
|
assert(strlist_contains(",,|x", '|', ",,"));
|
||||||
|
assert(strlist_contains("xyz", '\0', "xyz"));
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
(void) argc;
|
(void) argc;
|
||||||
(void) argv;
|
(void) argv;
|
||||||
@@ -304,5 +316,6 @@ int main(int argc, char *argv[]) {
|
|||||||
test_parse_integer();
|
test_parse_integer();
|
||||||
test_parse_integers();
|
test_parse_integers();
|
||||||
test_parse_integer_with_suffix();
|
test_parse_integer_with_suffix();
|
||||||
|
test_strlist_contains();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ControlMessage createBackOrScreenOn(int action) {
|
||||||
|
ControlMessage msg = new ControlMessage();
|
||||||
|
msg.type = TYPE_BACK_OR_SCREEN_ON;
|
||||||
|
msg.action = action;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public static ControlMessage createSetClipboard(String text, boolean paste) {
|
public static ControlMessage createSetClipboard(String text, boolean paste) {
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_SET_CLIPBOARD;
|
msg.type = TYPE_SET_CLIPBOARD;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class ControlMessageReader {
|
|||||||
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
|
||||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
|
||||||
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
|
||||||
|
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
|
||||||
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
|
||||||
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
|
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 1;
|
||||||
|
|
||||||
@@ -66,13 +67,15 @@ public class ControlMessageReader {
|
|||||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||||
msg = parseInjectScrollEvent();
|
msg = parseInjectScrollEvent();
|
||||||
break;
|
break;
|
||||||
|
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||||
|
msg = parseBackOrScreenOnEvent();
|
||||||
|
break;
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
msg = parseSetClipboard();
|
msg = parseSetClipboard();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
||||||
msg = parseSetScreenPowerMode();
|
msg = parseSetScreenPowerMode();
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_COLLAPSE_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||||
@@ -150,6 +153,14 @@ public class ControlMessageReader {
|
|||||||
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ControlMessage parseBackOrScreenOnEvent() {
|
||||||
|
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int action = toUnsigned(buffer.get());
|
||||||
|
return ControlMessage.createBackOrScreenOn(action);
|
||||||
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetClipboard() {
|
private ControlMessage parseSetClipboard() {
|
||||||
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class Controller {
|
|||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||||
if (device.supportsInputEvents()) {
|
if (device.supportsInputEvents()) {
|
||||||
pressBackOrTurnScreenOn();
|
pressBackOrTurnScreenOn(msg.getAction());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
@@ -255,12 +255,22 @@ public class Controller {
|
|||||||
}, 200, TimeUnit.MILLISECONDS);
|
}, 200, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pressBackOrTurnScreenOn() {
|
private boolean pressBackOrTurnScreenOn(int action) {
|
||||||
int keycode = Device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
|
if (Device.isScreenOn()) {
|
||||||
if (keepPowerModeOff && keycode == KeyEvent.KEYCODE_POWER) {
|
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen is off
|
||||||
|
// Only press POWER on ACTION_DOWN
|
||||||
|
if (action != KeyEvent.ACTION_DOWN) {
|
||||||
|
// do nothing,
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepPowerModeOff) {
|
||||||
schedulePowerModeOff();
|
schedulePowerModeOff();
|
||||||
}
|
}
|
||||||
return device.injectKeycode(keycode);
|
return device.injectKeycode(KeyEvent.KEYCODE_POWER);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean setClipboard(String text, boolean paste) {
|
private boolean setClipboard(String text, boolean paste) {
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public final class Device {
|
|||||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||||
|
|
||||||
|
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||||
|
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||||
|
|
||||||
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
|
||||||
|
|
||||||
public interface RotationListener {
|
public interface RotationListener {
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ public final class ScreenInfo {
|
|||||||
|
|
||||||
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||||
int rotation = displayInfo.getRotation();
|
int rotation = displayInfo.getRotation();
|
||||||
|
|
||||||
|
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
|
||||||
|
// The user requested to lock the video orientation to the current orientation
|
||||||
|
lockedVideoOrientation = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
Size deviceSize = displayInfo.getSize();
|
Size deviceSize = displayInfo.getSize();
|
||||||
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
|
||||||
if (crop != null) {
|
if (crop != null) {
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ public class ControlMessageReaderTest {
|
|||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
|
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
|
||||||
|
dos.writeByte(KeyEvent.ACTION_UP);
|
||||||
|
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
@@ -161,6 +162,7 @@ public class ControlMessageReaderTest {
|
|||||||
ControlMessage event = reader.next();
|
ControlMessage event = reader.next();
|
||||||
|
|
||||||
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
|
||||||
|
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user