mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-02-28 17:24:29 +01:00
Compare commits
28 Commits
docker
...
camera_con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bacd1a0c5f | ||
|
|
30203b527d | ||
|
|
58e4905261 | ||
|
|
31fbd9bfb1 | ||
|
|
ce5a2aa224 | ||
|
|
8cb601f602 | ||
|
|
dc628cc433 | ||
|
|
13e2e3d36b | ||
|
|
0a81dd52ea | ||
|
|
70ab117904 | ||
|
|
05379133a4 | ||
|
|
b04032d6f7 | ||
|
|
8e4f38c401 | ||
|
|
c21b36c255 | ||
|
|
5560e39559 | ||
|
|
7840c48043 | ||
|
|
599c17a5a2 | ||
|
|
db5c9e83ef | ||
|
|
1c602f88e5 | ||
|
|
d9791c506f | ||
|
|
4fd28755e0 | ||
|
|
5853adc860 | ||
|
|
7ec2e611dd | ||
|
|
8d4770a6c1 | ||
|
|
a0b4b3d36e | ||
|
|
ba6fa134af | ||
|
|
ee8534dccd | ||
|
|
750bb1a61b |
38
.github/workflows/release.yml
vendored
38
.github/workflows/release.yml
vendored
@@ -72,19 +72,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Same as build-linux-x86_64
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
|
||||
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
|
||||
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
|
||||
libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
libv4l-dev
|
||||
|
||||
# SDL3 is not available in Ubuntu yet
|
||||
- name: Install SDL3
|
||||
@@ -112,19 +106,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# https://wiki.libsdl.org/SDL3/README-linux#build-dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libavcodec-dev \
|
||||
libavdevice-dev libavformat-dev libavutil-dev libswresample-dev \
|
||||
libusb-1.0-0 libusb-1.0-0-dev libv4l-dev \
|
||||
libasound2-dev libpulse-dev \
|
||||
libaudio-dev libfribidi-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
|
||||
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
|
||||
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
|
||||
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev libthai-dev \
|
||||
libpipewire-0.3-dev libwayland-dev libdecor-0-dev liburing-dev
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
libv4l-dev
|
||||
|
||||
- name: Build
|
||||
run: release/build_linux.sh x86_64
|
||||
@@ -152,7 +140,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm \
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||
|
||||
- name: Build
|
||||
@@ -181,7 +171,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y meson ninja-build nasm \
|
||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||
|
||||
- name: Build
|
||||
@@ -245,7 +237,7 @@ jobs:
|
||||
path: release/work/build-macos-aarch64/dist-tar/
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-15-intel
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Check architecture
|
||||
run: |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source for the project. Do not download releases from random websites, even if
|
||||
their name contains `scrcpy`.**
|
||||
|
||||
# scrcpy (v3.3.4)
|
||||
# scrcpy (v3.3.3)
|
||||
|
||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||
|
||||
@@ -129,7 +129,7 @@ Here are just some common examples.
|
||||
scrcpy --otg
|
||||
```
|
||||
|
||||
- Control the device using gamepads plugged into the computer:
|
||||
- Control the device using gamepad controllers plugged into the computer:
|
||||
|
||||
```bash
|
||||
scrcpy --gamepad=uhid
|
||||
|
||||
@@ -18,6 +18,8 @@ _scrcpy() {
|
||||
--camera-fps=
|
||||
--camera-high-speed
|
||||
--camera-size=
|
||||
--camera-torch
|
||||
--camera-zoom=
|
||||
--capture-orientation=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
@@ -197,6 +199,8 @@ _scrcpy() {
|
||||
|--camera-id \
|
||||
|--camera-fps \
|
||||
|--camera-size \
|
||||
|--camera-torch \
|
||||
|--camera-zoom \
|
||||
|--crop \
|
||||
|--display-id \
|
||||
|--max-fps \
|
||||
|
||||
@@ -25,6 +25,8 @@ arguments=(
|
||||
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
|
||||
'--camera-fps=[Specify the camera capture frame rate]'
|
||||
'--camera-size=[Specify an explicit camera capture size]'
|
||||
'--camera-torch[Turn on the camera torch when the camera starts]'
|
||||
'--camera-zoom[Specify the camera zoom initial value]'
|
||||
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
|
||||
@@ -3,9 +3,9 @@ set -ex
|
||||
. $(dirname ${BASH_SOURCE[0]})/_init
|
||||
process_args "$@"
|
||||
|
||||
VERSION=3.4.0
|
||||
VERSION=3.2.18
|
||||
URL="https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz"
|
||||
SHA256SUM=9614b9696abc4597ffce6b888829dc6537ae500423474c342ac4a67222c5654c
|
||||
SHA256SUM=51539fa13e546bc50c632beed3f34257de2baa38a4c642048de56377903b4265
|
||||
|
||||
PROJECT_DIR="sdl-$VERSION"
|
||||
FILENAME="$PROJECT_DIR.tar.gz"
|
||||
@@ -37,7 +37,6 @@ else
|
||||
|
||||
conf=(
|
||||
-DCMAKE_INSTALL_PREFIX="$INSTALL_DIR/$DIRNAME"
|
||||
-DSDL_TESTS=OFF
|
||||
)
|
||||
|
||||
if [[ "$HOST" == linux ]]
|
||||
|
||||
@@ -276,7 +276,7 @@ if get_option('buildtype') == 'debug'
|
||||
exe = executable(t[0], sources,
|
||||
include_directories: src_dir,
|
||||
dependencies: dependencies,
|
||||
c_args: ['-DSC_TEST'])
|
||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
||||
test(t[0], exe)
|
||||
endforeach
|
||||
endif
|
||||
|
||||
@@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "3.3.4"
|
||||
VALUE "ProductVersion", "3.3.3"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
16
app/scrcpy.1
16
app/scrcpy.1
@@ -131,6 +131,14 @@ The available camera ids can be listed by \fB\-\-list\-cameras\fR.
|
||||
.BI "\-\-camera\-size " width\fRx\fIheight
|
||||
Specify an explicit camera capture size.
|
||||
|
||||
.TP
|
||||
.BI \-\-camera\-torch
|
||||
Turn on the camera torch when the camera starts.
|
||||
|
||||
.TP
|
||||
.BI "\-\-camera-zoom " zoom
|
||||
Specify the camera zoom initial value.
|
||||
|
||||
.TP
|
||||
.BI "\-\-capture\-orientation " value
|
||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
|
||||
@@ -815,6 +823,14 @@ Install APK from computer
|
||||
.B Drag & drop non-APK file
|
||||
Push file to device (see \fB\-\-push\-target\fR)
|
||||
|
||||
.TP
|
||||
.B MOD+l
|
||||
Turn on the camera torch (camera mode only)
|
||||
|
||||
.TP
|
||||
.B MOD+Shift+l
|
||||
Turn off the camera torch (camera mode only)
|
||||
|
||||
|
||||
.SH Environment variables
|
||||
|
||||
|
||||
@@ -47,7 +47,10 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
|
||||
static bool
|
||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
(void) session;
|
||||
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
|
||||
@@ -114,6 +114,8 @@ enum {
|
||||
OPT_NO_VD_SYSTEM_DECORATIONS,
|
||||
OPT_NO_VD_DESTROY_CONTENT,
|
||||
OPT_DISPLAY_IME_POLICY,
|
||||
OPT_CAMERA_TORCH,
|
||||
OPT_CAMERA_ZOOM,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@@ -313,6 +315,17 @@ static const struct sc_option options[] = {
|
||||
.argdesc = "<width>x<height>",
|
||||
.text = "Specify an explicit camera capture size.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_TORCH,
|
||||
.longopt = "camera-torch",
|
||||
.text = "Turn on the camera torch when the camera starts.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAMERA_ZOOM,
|
||||
.longopt = "camera-zoom",
|
||||
.argdesc = "zoom",
|
||||
.text = "Specify the camera zoom initial value.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CAPTURE_ORIENTATION,
|
||||
.longopt = "capture-orientation",
|
||||
@@ -1207,6 +1220,14 @@ static const struct sc_shortcut shortcuts[] = {
|
||||
.shortcuts = { "Drag & drop non-APK file" },
|
||||
.text = "Push file to device (see --push-target)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+l" },
|
||||
.text = "Turn on the camera torch (camera mode only)",
|
||||
},
|
||||
{
|
||||
.shortcuts = { "MOD+Shift+l" },
|
||||
.text = "Turn off the camera torch (camera mode only)",
|
||||
},
|
||||
};
|
||||
|
||||
static const struct sc_envvar envvars[] = {
|
||||
@@ -2780,6 +2801,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_CAMERA_HIGH_SPEED:
|
||||
opts->camera_high_speed = true;
|
||||
break;
|
||||
case OPT_CAMERA_TORCH:
|
||||
opts->camera_torch = true;
|
||||
break;
|
||||
case OPT_CAMERA_ZOOM:
|
||||
opts->camera_zoom = optarg;
|
||||
break;
|
||||
case OPT_NO_WINDOW:
|
||||
opts->window = false;
|
||||
break;
|
||||
@@ -2928,7 +2955,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
#endif
|
||||
|
||||
if (opts->control) {
|
||||
if (opts->control && opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
|
||||
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
|
||||
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
|
||||
: SC_KEYBOARD_INPUT_MODE_SDK;
|
||||
@@ -3106,8 +3133,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->control) {
|
||||
LOGI("Camera video source: control disabled");
|
||||
opts->control = false;
|
||||
// Disable all inputs for camera
|
||||
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_DISABLED;
|
||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
|
||||
opts->gamepad_input_mode = SC_GAMEPAD_INPUT_MODE_DISABLED;
|
||||
}
|
||||
} else if (opts->camera_id
|
||||
|| opts->camera_ar
|
||||
|
||||
@@ -182,12 +182,17 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
||||
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
|
||||
return 1 + len;
|
||||
}
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
|
||||
buf[1] = msg->camera_set_torch.on ? 1 : 0;
|
||||
return 2;
|
||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
||||
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
||||
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
|
||||
// no additional data
|
||||
return 1;
|
||||
default:
|
||||
@@ -318,6 +323,16 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
||||
LOG_CMSG("reset video");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH:
|
||||
LOG_CMSG("camera set torch %s",
|
||||
msg->camera_set_torch.on ? "on" : "off");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN:
|
||||
LOG_CMSG("camera zoom in");
|
||||
break;
|
||||
case SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT:
|
||||
LOG_CMSG("camera zoom out");
|
||||
break;
|
||||
default:
|
||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
||||
break;
|
||||
|
||||
@@ -43,6 +43,9 @@ enum sc_control_msg_type {
|
||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
||||
SC_CONTROL_MSG_TYPE_START_APP,
|
||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
|
||||
enum sc_copy_key {
|
||||
@@ -111,6 +114,9 @@ struct sc_control_msg {
|
||||
struct {
|
||||
char *name;
|
||||
} start_app;
|
||||
struct {
|
||||
bool on;
|
||||
} camera_set_torch;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -10,20 +10,30 @@
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
||||
|
||||
static bool
|
||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
|
||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
decoder->frame = av_frame_alloc();
|
||||
if (!decoder->frame) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
|
||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx, session)) {
|
||||
av_frame_free(&decoder->frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
decoder->ctx = ctx;
|
||||
|
||||
// A video stream must have a session
|
||||
assert(session || ctx->codec_type != AVMEDIA_TYPE_VIDEO);
|
||||
|
||||
if (session) {
|
||||
decoder->session = *session;
|
||||
}
|
||||
|
||||
memset(&decoder->frame_size, 0, sizeof(decoder->frame_size));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -61,6 +71,32 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
// a frame was received
|
||||
|
||||
if (decoder->ctx->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
assert(decoder->frame->width >= 0);
|
||||
assert(decoder->frame->height >= 0);
|
||||
struct sc_size frame_size = {
|
||||
.width = decoder->frame->width,
|
||||
.height = decoder->frame->height,
|
||||
};
|
||||
if (decoder->frame_size.width != frame_size.width
|
||||
|| decoder->frame_size.height != frame_size.height) {
|
||||
// The frame size has changed, check if it matches the session
|
||||
uint32_t sw = decoder->session.video.width;
|
||||
uint32_t sh = decoder->session.video.height;
|
||||
if (frame_size.width != sw || frame_size.height != sh) {
|
||||
LOGW("Unexpected video size: %" PRIu32 "x%" PRIu32
|
||||
" (expected %" PRIu32 "x%" PRIu32 ")",
|
||||
frame_size.width, frame_size.height, sw, sh);
|
||||
|
||||
LOGW("The encoder did not respect the requested size, "
|
||||
"please retry with a lower resolution (-m/--max-size)");
|
||||
}
|
||||
}
|
||||
|
||||
decoder->frame_size = frame_size;
|
||||
}
|
||||
|
||||
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
|
||||
decoder->frame);
|
||||
av_frame_unref(decoder->frame);
|
||||
@@ -74,9 +110,17 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
|
||||
sc_decoder_push_session(struct sc_decoder *decoder,
|
||||
const struct sc_stream_session *session) {
|
||||
decoder->session = *session;
|
||||
return sc_frame_source_sinks_push_session(&decoder->frame_source, session);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
||||
return sc_decoder_open(decoder, ctx);
|
||||
return sc_decoder_open(decoder, ctx, session);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -92,6 +136,14 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
return sc_decoder_push(decoder, packet);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_decoder_packet_sink_push_session(struct sc_packet_sink *sink,
|
||||
const struct sc_stream_session *session) {
|
||||
|
||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
||||
return sc_decoder_push_session(decoder, session);
|
||||
}
|
||||
|
||||
void
|
||||
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||
decoder->name = name; // statically allocated
|
||||
@@ -101,6 +153,7 @@ sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||
.open = sc_decoder_packet_sink_open,
|
||||
.close = sc_decoder_packet_sink_close,
|
||||
.push = sc_decoder_packet_sink_push,
|
||||
.push_session = sc_decoder_packet_sink_push_session,
|
||||
};
|
||||
|
||||
decoder->packet_sink.ops = &ops;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
#include "coords.h"
|
||||
#include "trait/frame_source.h"
|
||||
#include "trait/packet_sink.h"
|
||||
|
||||
@@ -16,6 +17,9 @@ struct sc_decoder {
|
||||
|
||||
AVCodecContext *ctx;
|
||||
AVFrame *frame;
|
||||
|
||||
struct sc_stream_session session; // only initialized for video stream
|
||||
struct sc_size frame_size;
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
|
||||
@@ -10,16 +10,18 @@
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
|
||||
|
||||
static bool
|
||||
sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
|
||||
dframe->frame = av_frame_alloc();
|
||||
if (!dframe->frame) {
|
||||
sc_delayed_packet_init_frame(struct sc_delayed_packet *dpacket,
|
||||
const AVFrame *frame) {
|
||||
dpacket->type = SC_DELAYED_PACKET_TYPE_FRAME;
|
||||
dpacket->frame = av_frame_alloc();
|
||||
if (!dpacket->frame) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (av_frame_ref(dframe->frame, frame)) {
|
||||
if (av_frame_ref(dpacket->frame, frame)) {
|
||||
LOG_OOM();
|
||||
av_frame_free(&dframe->frame);
|
||||
av_frame_free(&dpacket->frame);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -27,9 +29,18 @@ sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
|
||||
}
|
||||
|
||||
static void
|
||||
sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) {
|
||||
av_frame_unref(dframe->frame);
|
||||
av_frame_free(&dframe->frame);
|
||||
sc_delayed_packet_init_session(struct sc_delayed_packet *dpacket,
|
||||
const struct sc_stream_session *session) {
|
||||
dpacket->type = SC_DELAYED_PACKET_TYPE_SESSION;
|
||||
dpacket->session = *session;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_delayed_packet_destroy(struct sc_delayed_packet *dpacket) {
|
||||
if (dpacket->type == SC_DELAYED_PACKET_TYPE_FRAME) {
|
||||
av_frame_unref(dpacket->frame);
|
||||
av_frame_free(&dpacket->frame);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
@@ -50,43 +61,52 @@ run_buffering(void *data) {
|
||||
goto stopped;
|
||||
}
|
||||
|
||||
struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue);
|
||||
struct sc_delayed_packet dpacket = sc_vecdeque_pop(&db->queue);
|
||||
|
||||
sc_tick max_deadline = sc_tick_now() + db->delay;
|
||||
// PTS (written by the server) are expressed in microseconds
|
||||
sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts);
|
||||
bool ok;
|
||||
if (dpacket.type == SC_DELAYED_PACKET_TYPE_FRAME) {
|
||||
sc_tick max_deadline = sc_tick_now() + db->delay;
|
||||
// PTS (written by the server) are expressed in microseconds
|
||||
sc_tick pts = SC_TICK_FROM_US(dpacket.frame->pts);
|
||||
|
||||
bool timed_out = false;
|
||||
while (!db->stopped && !timed_out) {
|
||||
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
|
||||
+ db->delay;
|
||||
if (deadline > max_deadline) {
|
||||
deadline = max_deadline;
|
||||
bool timed_out = false;
|
||||
while (!db->stopped && !timed_out) {
|
||||
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
|
||||
+ db->delay;
|
||||
if (deadline > max_deadline) {
|
||||
deadline = max_deadline;
|
||||
}
|
||||
|
||||
timed_out =
|
||||
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
|
||||
}
|
||||
|
||||
timed_out =
|
||||
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
|
||||
}
|
||||
bool stopped = db->stopped;
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
|
||||
bool stopped = db->stopped;
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
|
||||
if (stopped) {
|
||||
sc_delayed_frame_destroy(&dframe);
|
||||
goto stopped;
|
||||
}
|
||||
if (stopped) {
|
||||
sc_delayed_packet_destroy(&dpacket);
|
||||
goto stopped;
|
||||
}
|
||||
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
||||
pts, dframe.push_date, sc_tick_now());
|
||||
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
||||
pts, dframe.push_date, sc_tick_now());
|
||||
#endif
|
||||
|
||||
bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame);
|
||||
sc_delayed_frame_destroy(&dframe);
|
||||
ok = sc_frame_source_sinks_push(&db->frame_source, dpacket.frame);
|
||||
} else {
|
||||
assert(dpacket.type == SC_DELAYED_PACKET_TYPE_SESSION);
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
ok = sc_frame_source_sinks_push_session(&db->frame_source,
|
||||
&dpacket.session);
|
||||
}
|
||||
|
||||
sc_delayed_packet_destroy(&dpacket);
|
||||
if (!ok) {
|
||||
LOGE("Delayed frame could not be pushed, stopping");
|
||||
LOGE("Delayed packet could not be pushed, stopping");
|
||||
sc_mutex_lock(&db->mutex);
|
||||
// Prevent to push any new frame
|
||||
// Prevent to push any new packet
|
||||
db->stopped = true;
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
goto stopped;
|
||||
@@ -98,8 +118,8 @@ stopped:
|
||||
|
||||
// Flush queue
|
||||
while (!sc_vecdeque_is_empty(&db->queue)) {
|
||||
struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue);
|
||||
sc_delayed_frame_destroy(dframe);
|
||||
struct sc_delayed_packet *dpacket = sc_vecdeque_popref(&db->queue);
|
||||
sc_delayed_packet_destroy(dpacket);
|
||||
}
|
||||
|
||||
LOGD("Buffering thread ended");
|
||||
@@ -109,9 +129,11 @@ stopped:
|
||||
|
||||
static bool
|
||||
sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
struct sc_delay_buffer *db = DOWNCAST(sink);
|
||||
(void) ctx;
|
||||
(void) session;
|
||||
|
||||
bool ok = sc_mutex_init(&db->mutex);
|
||||
if (!ok) {
|
||||
@@ -132,7 +154,7 @@ sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
|
||||
sc_vecdeque_init(&db->queue);
|
||||
db->stopped = false;
|
||||
|
||||
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
|
||||
if (!sc_frame_source_sinks_open(&db->frame_source, ctx, session)) {
|
||||
goto error_destroy_wait_cond;
|
||||
}
|
||||
|
||||
@@ -196,24 +218,56 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||
}
|
||||
|
||||
struct sc_delayed_frame dframe;
|
||||
bool ok = sc_delayed_frame_init(&dframe, frame);
|
||||
if (!ok) {
|
||||
struct sc_delayed_packet *dpacket = sc_vecdeque_push_hole(&db->queue);
|
||||
if (!dpacket) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
dframe.push_date = sc_tick_now();
|
||||
#endif
|
||||
|
||||
ok = sc_vecdeque_push(&db->queue, dframe);
|
||||
bool ok = sc_delayed_packet_init_frame(dpacket, frame);
|
||||
if (!ok) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
dpacket->push_date = sc_tick_now();
|
||||
#endif
|
||||
|
||||
sc_cond_signal(&db->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_delay_buffer_frame_sink_push_session(struct sc_frame_sink *sink,
|
||||
const struct sc_stream_session *session) {
|
||||
struct sc_delay_buffer *db = DOWNCAST(sink);
|
||||
|
||||
sc_mutex_lock(&db->mutex);
|
||||
|
||||
if (db->stopped) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sc_delayed_packet *dpacket = sc_vecdeque_push_hole(&db->queue);
|
||||
if (!dpacket) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_delayed_packet_init_session(dpacket, session);
|
||||
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
dpacket->push_date = sc_tick_now();
|
||||
#endif
|
||||
|
||||
sc_cond_signal(&db->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
@@ -235,6 +289,7 @@ sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
|
||||
.open = sc_delay_buffer_frame_sink_open,
|
||||
.close = sc_delay_buffer_frame_sink_close,
|
||||
.push = sc_delay_buffer_frame_sink_push,
|
||||
.push_session = sc_delay_buffer_frame_sink_push_session,
|
||||
};
|
||||
|
||||
db->frame_sink.ops = &ops;
|
||||
|
||||
@@ -18,14 +18,23 @@
|
||||
// forward declarations
|
||||
typedef struct AVFrame AVFrame;
|
||||
|
||||
struct sc_delayed_frame {
|
||||
AVFrame *frame;
|
||||
enum sc_delayed_packet_type {
|
||||
SC_DELAYED_PACKET_TYPE_FRAME,
|
||||
SC_DELAYED_PACKET_TYPE_SESSION,
|
||||
};
|
||||
|
||||
struct sc_delayed_packet {
|
||||
enum sc_delayed_packet_type type;
|
||||
union {
|
||||
AVFrame *frame;
|
||||
struct sc_stream_session session;
|
||||
};
|
||||
#ifdef SC_BUFFERING_DEBUG
|
||||
sc_tick push_date;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame);
|
||||
struct sc_delayed_packet_queue SC_VECDEQUE(struct sc_delayed_packet);
|
||||
|
||||
struct sc_delay_buffer {
|
||||
struct sc_frame_source frame_source; // frame source trait
|
||||
@@ -40,7 +49,7 @@ struct sc_delay_buffer {
|
||||
sc_cond wait_cond;
|
||||
|
||||
struct sc_clock clock;
|
||||
struct sc_delayed_frame_queue queue;
|
||||
struct sc_delayed_packet_queue queue;
|
||||
bool stopped;
|
||||
};
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
#define SC_PACKET_HEADER_SIZE 12
|
||||
|
||||
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63)
|
||||
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62)
|
||||
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 62)
|
||||
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 61)
|
||||
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
@@ -63,48 +63,75 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
|
||||
uint32_t *height) {
|
||||
uint8_t data[8];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 8);
|
||||
if (r < 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = sc_read32be(data);
|
||||
*height = sc_read32be(data + 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
static inline bool
|
||||
sc_demuxer_recv_header(struct sc_demuxer *demuxer,
|
||||
uint8_t buf[static SC_PACKET_HEADER_SIZE]) {
|
||||
// The video and audio streams contain a sequence of raw packets (as
|
||||
// provided by MediaCodec), each prefixed with a "meta" header.
|
||||
//
|
||||
// The "meta" header length is 12 bytes:
|
||||
// The "meta" header length is 12 bytes.
|
||||
//
|
||||
//
|
||||
// If the MSB is 1, then it is a session packet (for a video stream only),
|
||||
// which only contains a 12-byte header:
|
||||
//
|
||||
// byte 0 byte 1 byte 2 byte 3
|
||||
// 10000000 00000000 00000000 00000000
|
||||
// ^<-------------------------------->
|
||||
// | padding
|
||||
// `- session packet flag
|
||||
//
|
||||
// byte 4 byte 5 byte 6 byte 7 byte 8 byte 9 byte 10 byte 11
|
||||
// ........ ........ ........ ........ ........ ........ ........ ........
|
||||
// <---------------------------------> <--------------------------------->
|
||||
// video width video height
|
||||
//
|
||||
//
|
||||
// If the MSB is 0, then it is a media packet, comprised of a 12-byte header
|
||||
// followed by <packet_size> bytes containing the packet/frame:
|
||||
//
|
||||
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
|
||||
// <-------------> <-----> <-----------------------------...
|
||||
// PTS packet raw packet
|
||||
// size
|
||||
//
|
||||
// It is followed by <packet_size> bytes containing the packet/frame.
|
||||
//
|
||||
// The most significant bits of the PTS are used for packet flags:
|
||||
//
|
||||
// byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
|
||||
// CK...... ........ ........ ........ ........ ........ ........ ........
|
||||
// ^^<------------------------------------------------------------------->
|
||||
// || PTS
|
||||
// | `- key frame
|
||||
// `-- config packet
|
||||
// byte 0 byte 1 byte 2 byte 3 byte 4 byte 5 byte 6 byte 7
|
||||
// 0CK..... ........ ........ ........ ........ ........ ........ ........
|
||||
// ^^^<------------------------------------------------------------------>
|
||||
// ||| PTS
|
||||
// || `- key frame
|
||||
// | `-- config packet
|
||||
// `--- media packet flag
|
||||
//
|
||||
// byte 8 byte 9 byte 10 byte 11
|
||||
// ........ ........ ........ ........ ........ ........ . . .
|
||||
// <---------------------------------> <---------------- . . .
|
||||
// packet size raw packet
|
||||
//
|
||||
ssize_t r = net_recv_all(demuxer->socket, buf, SC_PACKET_HEADER_SIZE);
|
||||
assert(r <= SC_PACKET_HEADER_SIZE);
|
||||
return r == SC_PACKET_HEADER_SIZE;
|
||||
}
|
||||
|
||||
uint8_t header[SC_PACKET_HEADER_SIZE];
|
||||
ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
|
||||
if (r < SC_PACKET_HEADER_SIZE) {
|
||||
return false;
|
||||
}
|
||||
static bool
|
||||
sc_demuxer_is_session(const uint8_t *header) {
|
||||
return header[0] & 0x80;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_demuxer_parse_session(const uint8_t *header,
|
||||
struct sc_stream_session *session) {
|
||||
assert(sc_demuxer_is_session(header));
|
||||
session->video.width = sc_read32be(&header[4]);
|
||||
session->video.height = sc_read32be(&header[8]);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, const uint8_t *header,
|
||||
AVPacket *packet) {
|
||||
assert(!sc_demuxer_is_session(header));
|
||||
uint64_t pts_flags = sc_read64be(header);
|
||||
uint32_t len = sc_read32be(&header[8]);
|
||||
assert(len);
|
||||
@@ -114,7 +141,7 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
r = net_recv_all(demuxer->socket, packet->data, len);
|
||||
ssize_t r = net_recv_all(demuxer->socket, packet->data, len);
|
||||
if (r < 0 || ((uint32_t) r) < len) {
|
||||
av_packet_unref(packet);
|
||||
return false;
|
||||
@@ -187,17 +214,28 @@ run_demuxer(void *data) {
|
||||
|
||||
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
uint8_t header[SC_PACKET_HEADER_SIZE];
|
||||
struct sc_stream_session session_data;
|
||||
|
||||
struct sc_stream_session *session = NULL;
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
|
||||
bool ok = sc_demuxer_recv_header(demuxer, header);
|
||||
if (!ok) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
codec_ctx->width = width;
|
||||
codec_ctx->height = height;
|
||||
if (!sc_demuxer_is_session(header)) {
|
||||
LOGE("Unexpected packet (not a session header)");
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
session = &session_data;
|
||||
sc_demuxer_parse_session(header, session);
|
||||
|
||||
codec_ctx->width = session_data.video.width;
|
||||
codec_ctx->height = session_data.video.height;
|
||||
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
|
||||
} else {
|
||||
// Hardcoded audio properties
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
@@ -219,7 +257,8 @@ run_demuxer(void *data) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
|
||||
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx,
|
||||
session)) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
@@ -241,27 +280,39 @@ run_demuxer(void *data) {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||
bool ok = sc_demuxer_recv_header(demuxer, header);
|
||||
if (!ok) {
|
||||
// end of stream
|
||||
status = SC_DEMUXER_STATUS_EOS;
|
||||
break;
|
||||
}
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (sc_demuxer_is_session(header)) {
|
||||
sc_demuxer_parse_session(header, &session_data);
|
||||
ok = sc_packet_source_sinks_push_session(&demuxer->packet_source,
|
||||
&session_data);
|
||||
if (!ok) {
|
||||
av_packet_unref(packet);
|
||||
// The sink already logged its concrete error
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sc_demuxer_recv_packet(demuxer, header, packet);
|
||||
|
||||
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
// The sink already logged its concrete error
|
||||
break;
|
||||
if (must_merge_config_packet) {
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (!ok) {
|
||||
av_packet_unref(packet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
// The sink already logged its concrete error
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ sc_display_update_texture_internal(struct sc_display *display,
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
|
||||
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
|
||||
gl->GenerateMipmap(GL_TEXTURE_2D);
|
||||
display->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||
gl->BindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include "hid/hid_event.h"
|
||||
#include "input_events.h"
|
||||
|
||||
// See "SDL3/SDL_scancode.h".
|
||||
// See "SDL2/SDL_scancode.h".
|
||||
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
|
||||
// HID protocol.
|
||||
// 0x65 is Application, typically AT-101 Keyboard ends here.
|
||||
|
||||
@@ -261,7 +261,7 @@ load_from_path(const char *path) {
|
||||
// frame owns the data
|
||||
bool ok = SDL_SetPointerProperty(props, "sc_frame", frame);
|
||||
if (!ok) {
|
||||
LOGE("Could not set pointer property: %s", SDL_GetError());
|
||||
LOGE("Could not get surface properties: %s", SDL_GetError());
|
||||
SDL_DestroySurface(surface);
|
||||
goto error;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
* for simplicity.
|
||||
*
|
||||
* This scrcpy input events API is designed to be consumed by input event
|
||||
* processors (sc_key_processor, sc_mouse_processor and sc_gamepad_processor,
|
||||
* see app/src/trait/).
|
||||
* processors (sc_key_processor and sc_mouse_processor, see app/src/trait/).
|
||||
*
|
||||
* One major semantic difference between SDL input events and scrcpy input
|
||||
* events is their frame of reference (for mouse and touch events): SDL events
|
||||
@@ -323,6 +322,9 @@ enum sc_mouse_button {
|
||||
SC_MOUSE_BUTTON_X2 = SDL_BUTTON_MASK(SDL_BUTTON_X2),
|
||||
};
|
||||
|
||||
// Use the naming from SDL3 for gamepad axis and buttons:
|
||||
// <https://wiki.libsdl.org/SDL3/README/migration>
|
||||
|
||||
enum sc_gamepad_axis {
|
||||
SC_GAMEPAD_AXIS_UNKNOWN = -1,
|
||||
SC_GAMEPAD_AXIS_LEFTX = SDL_GAMEPAD_AXIS_LEFTX,
|
||||
@@ -409,9 +411,10 @@ struct sc_touch_event {
|
||||
float pressure;
|
||||
};
|
||||
|
||||
// As documented in <https://wiki.libsdl.org/SDL3/SDL_JoystickID>:
|
||||
// The value 0 is an invalid ID.
|
||||
#define SC_GAMEPAD_ID_INVALID 0
|
||||
// As documented in <https://wiki.libsdl.org/SDL2/SDL_JoystickID>:
|
||||
// The ID value starts at 0 and increments from there. The value -1 is an
|
||||
// invalid ID.
|
||||
#define SC_GAMEPAD_ID_INVALID UINT32_C(-1)
|
||||
|
||||
struct sc_gamepad_device_event {
|
||||
uint32_t gamepad_id;
|
||||
@@ -497,7 +500,7 @@ static inline enum sc_gamepad_axis
|
||||
sc_gamepad_axis_from_sdl(uint8_t axis) {
|
||||
if (axis <= SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||
// SC_GAMEPAD_AXIS_* constants are initialized from
|
||||
// SDL_GAMEPAD_AXIS_*
|
||||
// SDL_CONTROLLER_AXIS_*
|
||||
return axis;
|
||||
}
|
||||
return SC_GAMEPAD_AXIS_UNKNOWN;
|
||||
@@ -507,14 +510,14 @@ static inline enum sc_gamepad_button
|
||||
sc_gamepad_button_from_sdl(uint8_t button) {
|
||||
if (button <= SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
|
||||
// SC_GAMEPAD_BUTTON_* constants are initialized from
|
||||
// SDL_GAMEPAD_BUTTON_*
|
||||
// SDL_CONTROLLER_BUTTON_*
|
||||
return button;
|
||||
}
|
||||
return SC_GAMEPAD_BUTTON_UNKNOWN;
|
||||
}
|
||||
|
||||
static inline enum sc_action
|
||||
sc_action_from_sdl_gamepad_button_type(uint32_t type) {
|
||||
sc_action_from_sdl_controllerbutton_type(uint32_t type) {
|
||||
assert(type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || type == SDL_EVENT_GAMEPAD_BUTTON_UP);
|
||||
if (type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||
return SC_ACTION_DOWN;
|
||||
|
||||
@@ -29,6 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
im->kp = params->kp;
|
||||
im->mp = params->mp;
|
||||
im->gp = params->gp;
|
||||
im->camera = params->camera;
|
||||
|
||||
im->mouse_bindings = params->mouse_bindings;
|
||||
im->legacy_paste = params->legacy_paste;
|
||||
@@ -52,7 +53,7 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
static void
|
||||
send_keycode(struct sc_input_manager *im, enum android_keycode keycode,
|
||||
enum sc_action action, const char *name) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
// send DOWN event
|
||||
struct sc_control_msg msg;
|
||||
@@ -109,7 +110,7 @@ action_menu(struct sc_input_manager *im, enum sc_action action) {
|
||||
static void
|
||||
press_back_or_turn_screen_on(struct sc_input_manager *im,
|
||||
enum sc_action action) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
|
||||
@@ -124,7 +125,7 @@ press_back_or_turn_screen_on(struct sc_input_manager *im,
|
||||
|
||||
static void
|
||||
expand_notification_panel(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
|
||||
@@ -136,7 +137,7 @@ expand_notification_panel(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
expand_settings_panel(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
|
||||
@@ -148,7 +149,7 @@ expand_settings_panel(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
collapse_panels(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS;
|
||||
@@ -160,7 +161,7 @@ collapse_panels(struct sc_input_manager *im) {
|
||||
|
||||
static bool
|
||||
get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD;
|
||||
@@ -177,7 +178,7 @@ get_device_clipboard(struct sc_input_manager *im, enum sc_copy_key copy_key) {
|
||||
static bool
|
||||
set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
uint64_t sequence) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
@@ -209,7 +210,7 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
|
||||
|
||||
static void
|
||||
set_display_power(struct sc_input_manager *im, bool on) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||
@@ -236,7 +237,7 @@ switch_fps_counter_state(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
clipboard_paste(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->kp);
|
||||
assert(im->controller && im->kp && !im->camera);
|
||||
|
||||
char *text = SDL_GetClipboardText();
|
||||
if (!text) {
|
||||
@@ -267,7 +268,7 @@ clipboard_paste(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
rotate_device(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE;
|
||||
@@ -279,7 +280,7 @@ rotate_device(struct sc_input_manager *im) {
|
||||
|
||||
static void
|
||||
open_hard_keyboard_settings(struct sc_input_manager *im) {
|
||||
assert(im->controller);
|
||||
assert(im->controller && !im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS;
|
||||
@@ -301,6 +302,43 @@ reset_video(struct sc_input_manager *im) {
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_set_torch(struct sc_input_manager *im, bool on) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH;
|
||||
msg.camera_set_torch.on = on;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request setting camera torch");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_zoom_in(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request camera zoom in");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
camera_zoom_out(struct sc_input_manager *im) {
|
||||
assert(im->controller && im->camera);
|
||||
|
||||
struct sc_control_msg msg;
|
||||
msg.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
LOGW("Could not request camera zoom out");
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
apply_orientation_transform(struct sc_input_manager *im,
|
||||
enum sc_orientation transform) {
|
||||
@@ -313,6 +351,10 @@ apply_orientation_transform(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_text_input(struct sc_input_manager *im,
|
||||
const SDL_TextInputEvent *event) {
|
||||
if (im->camera || !im->kp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->kp->ops->process_text) {
|
||||
// The key processor does not support text input
|
||||
return;
|
||||
@@ -370,6 +412,9 @@ inverse_point(struct sc_point point, struct sc_size size,
|
||||
static void
|
||||
sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
const SDL_KeyboardEvent *event) {
|
||||
// some key events do not interact with the device, so process the event
|
||||
// even if control is disabled
|
||||
|
||||
// controller is NULL if --no-control is requested
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
@@ -403,106 +448,45 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
if (is_shortcut) {
|
||||
enum sc_action action = down ? SC_ACTION_DOWN : SC_ACTION_UP;
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_H:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_B: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_back(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_S:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_app_switch(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_M:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_menu(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_P:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_power(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_O:
|
||||
if (control && !repeat && down && !paused) {
|
||||
bool on = shift;
|
||||
set_display_power(im, on);
|
||||
}
|
||||
return;
|
||||
case SDLK_Z:
|
||||
if (video && down && !repeat) {
|
||||
sc_screen_set_paused(im->screen, !shift);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
// Only capture if shift is set
|
||||
if (shift) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_down(im, action);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
break;
|
||||
case SDLK_UP:
|
||||
// Only capture if shift is set
|
||||
if (shift) {
|
||||
if (video && !repeat && down) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_180);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_180);
|
||||
}
|
||||
} else if (im->kp && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_up(im, action);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
break;
|
||||
case SDLK_LEFT:
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_270);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_270);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_RIGHT:
|
||||
if (video && !repeat && down) {
|
||||
if (shift) {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_FLIP_0);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_FLIP_0);
|
||||
} else {
|
||||
apply_orientation_transform(im,
|
||||
SC_ORIENTATION_90);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_C:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_X:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_V:
|
||||
if (im->kp && !repeat && down && !paused) {
|
||||
if (shift || im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
clipboard_paste(im);
|
||||
} else {
|
||||
// store the text in the device clipboard and paste,
|
||||
// without requesting an acknowledgment
|
||||
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
|
||||
apply_orientation_transform(im, SC_ORIENTATION_90);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -526,33 +510,134 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
switch_fps_counter_state(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_N:
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
collapse_panels(im);
|
||||
} else if (im->key_repeat == 0) {
|
||||
expand_notification_panel(im);
|
||||
} else {
|
||||
expand_settings_panel(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_R:
|
||||
if (control && !repeat && down && !paused) {
|
||||
if (shift) {
|
||||
}
|
||||
|
||||
// Flatten conditions to avoid additional indentation levels
|
||||
if (control) {
|
||||
// Controls for all sources
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_R:
|
||||
if (!repeat && shift && down && !paused) {
|
||||
reset_video(im);
|
||||
} else {
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (control && !im->camera) {
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_H:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_home(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_B: // fall-through
|
||||
case SDLK_BACKSPACE:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_back(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_S:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_app_switch(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_M:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_menu(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_P:
|
||||
if (im->kp && !shift && !repeat && !paused) {
|
||||
action_power(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_O:
|
||||
if (control && !repeat && down && !paused) {
|
||||
bool on = shift;
|
||||
set_display_power(im, on);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (im->kp && !shift && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_down(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (im->kp && !shift && !paused) {
|
||||
// forward repeated events
|
||||
action_volume_up(im, action);
|
||||
}
|
||||
return;
|
||||
case SDLK_C:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_COPY);
|
||||
}
|
||||
return;
|
||||
case SDLK_X:
|
||||
if (im->kp && !shift && !repeat && down && !paused) {
|
||||
get_device_clipboard(im, SC_COPY_KEY_CUT);
|
||||
}
|
||||
return;
|
||||
case SDLK_V:
|
||||
if (im->kp && !repeat && down && !paused) {
|
||||
if (shift || im->legacy_paste) {
|
||||
// inject the text as input events
|
||||
clipboard_paste(im);
|
||||
} else {
|
||||
// store the text in the device clipboard and paste,
|
||||
// without requesting an acknowledgment
|
||||
set_device_clipboard(im, true, SC_SEQUENCE_INVALID);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_N:
|
||||
if (!repeat && down && !paused) {
|
||||
if (shift) {
|
||||
collapse_panels(im);
|
||||
} else if (im->key_repeat == 0) {
|
||||
expand_notification_panel(im);
|
||||
} else {
|
||||
expand_settings_panel(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_R:
|
||||
if (!repeat && !shift && down && !paused) {
|
||||
rotate_device(im);
|
||||
}
|
||||
}
|
||||
return;
|
||||
case SDLK_K:
|
||||
if (control && !shift && !repeat && down && !paused
|
||||
&& im->kp && im->kp->hid) {
|
||||
// Only if the current keyboard is hid
|
||||
open_hard_keyboard_settings(im);
|
||||
}
|
||||
return;
|
||||
return;
|
||||
case SDLK_K:
|
||||
if (!shift && !repeat && down && !paused
|
||||
&& im->kp && im->kp->hid) {
|
||||
// Only if the current keyboard is hid
|
||||
open_hard_keyboard_settings(im);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (control && im->camera) {
|
||||
switch (sdl_keycode) {
|
||||
case SDLK_L:
|
||||
if (!repeat && down) {
|
||||
camera_set_torch(im, !shift);
|
||||
}
|
||||
return;
|
||||
case SDLK_DOWN:
|
||||
if (!shift && down && !paused) {
|
||||
// forward repeated events
|
||||
camera_zoom_out(im);
|
||||
}
|
||||
return;
|
||||
case SDLK_UP:
|
||||
if (!shift && down && !paused) {
|
||||
// forward repeated events
|
||||
camera_zoom_in(im);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -562,6 +647,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
||||
return;
|
||||
}
|
||||
|
||||
assert(!im->camera);
|
||||
|
||||
uint64_t ack_to_wait = SC_SEQUENCE_INVALID;
|
||||
bool is_ctrl_v = ctrl && !shift && sdl_keycode == SDLK_V && down && !repeat;
|
||||
if (im->clipboard_autosync && is_ctrl_v) {
|
||||
@@ -633,6 +720,10 @@ sc_input_manager_get_position(struct sc_input_manager *im, int32_t x,
|
||||
static void
|
||||
sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
const SDL_MouseMotionEvent *event) {
|
||||
if (im->camera || !im->mp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
@@ -668,6 +759,10 @@ sc_input_manager_process_mouse_motion(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_touch(struct sc_input_manager *im,
|
||||
const SDL_TouchFingerEvent *event) {
|
||||
if (im->camera || !im->mp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->mp->ops->process_touch) {
|
||||
// The mouse processor does not support touch events
|
||||
return;
|
||||
@@ -716,6 +811,13 @@ sc_input_manager_get_binding(const struct sc_mouse_binding_set *bindings,
|
||||
static void
|
||||
sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
const SDL_MouseButtonEvent *event) {
|
||||
// some mouse events do not interact with the device, so process the event
|
||||
// even if control is disabled
|
||||
|
||||
if (im->camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->which == SDL_TOUCH_MOUSEID) {
|
||||
// simulated from touch events, so it's a duplicate
|
||||
return;
|
||||
@@ -883,6 +985,10 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
const SDL_MouseWheelEvent *event) {
|
||||
if (im->camera || !im->kp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!im->mp->ops->process_mouse_scroll) {
|
||||
// The mouse processor does not support scroll events
|
||||
return;
|
||||
@@ -907,17 +1013,23 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
const SDL_GamepadDeviceEvent *event) {
|
||||
// Handle device added or removed even if paused
|
||||
|
||||
if (im->camera || !im->gp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
|
||||
if (!sdl_gamepad) {
|
||||
LOGW("Could not open gamepad");
|
||||
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
|
||||
if (!gc) {
|
||||
LOGW("Could not open game controller");
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
|
||||
if (!joystick) {
|
||||
LOGW("Could not get gamepad joystick");
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
LOGW("Could not get controller joystick");
|
||||
SDL_CloseGamepad(gc);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -928,9 +1040,9 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
|
||||
if (sdl_gamepad) {
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
|
||||
if (gc) {
|
||||
SDL_CloseGamepad(gc);
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
@@ -948,6 +1060,10 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
const SDL_GamepadAxisEvent *event) {
|
||||
if (im->camera || !im->gp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
|
||||
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
|
||||
return;
|
||||
@@ -964,6 +1080,10 @@ sc_input_manager_process_gamepad_axis(struct sc_input_manager *im,
|
||||
static void
|
||||
sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
|
||||
const SDL_GamepadButtonEvent *event) {
|
||||
if (im->camera || !im->gp || im->screen->paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
|
||||
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
|
||||
return;
|
||||
@@ -971,7 +1091,7 @@ sc_input_manager_process_gamepad_button(struct sc_input_manager *im,
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_gamepad_button_type(event->type),
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.button = button,
|
||||
};
|
||||
im->gp->ops->process_gamepad_button(im->gp, &evt);
|
||||
@@ -986,6 +1106,10 @@ is_apk(const char *file) {
|
||||
static void
|
||||
sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
const SDL_DropEvent *event) {
|
||||
if (im->camera || !im->controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
assert(event->type == SDL_EVENT_DROP_FILE);
|
||||
char *file = strdup(event->data);
|
||||
if (!file) {
|
||||
@@ -1008,72 +1132,41 @@ sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event) {
|
||||
bool control = im->controller;
|
||||
bool paused = im->screen->paused;
|
||||
switch (event->type) {
|
||||
case SDL_EVENT_TEXT_INPUT:
|
||||
if (!im->kp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_text_input(im, &event->text);
|
||||
break;
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
case SDL_EVENT_KEY_UP:
|
||||
// some key events do not interact with the device, so process the
|
||||
// event even if control is disabled
|
||||
sc_input_manager_process_key(im, &event->key);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_motion(im, &event->motion);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_mouse_wheel(im, &event->wheel);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
// some mouse events do not interact with the device, so process
|
||||
// the event even if control is disabled
|
||||
sc_input_manager_process_mouse_button(im, &event->button);
|
||||
break;
|
||||
case SDL_EVENT_FINGER_MOTION:
|
||||
case SDL_EVENT_FINGER_DOWN:
|
||||
case SDL_EVENT_FINGER_UP:
|
||||
if (!im->mp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_touch(im, &event->tfinger);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||
// Handle device added or removed even if paused
|
||||
if (!im->gp) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_device(im, &event->gdevice);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
|
||||
if (!im->gp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_axis(im, &event->gaxis);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
if (!im->gp || paused) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_gamepad_button(im, &event->gbutton);
|
||||
break;
|
||||
case SDL_EVENT_DROP_FILE: {
|
||||
if (!control) {
|
||||
break;
|
||||
}
|
||||
sc_input_manager_process_file(im, &event->drop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ struct sc_input_manager {
|
||||
struct sc_mouse_processor *mp;
|
||||
struct sc_gamepad_processor *gp;
|
||||
|
||||
bool camera;
|
||||
|
||||
struct sc_mouse_bindings mouse_bindings;
|
||||
bool legacy_paste;
|
||||
bool clipboard_autosync;
|
||||
@@ -53,6 +55,7 @@ struct sc_input_manager_params {
|
||||
struct sc_key_processor *kp;
|
||||
struct sc_mouse_processor *mp;
|
||||
struct sc_gamepad_processor *gp;
|
||||
bool camera;
|
||||
|
||||
struct sc_mouse_bindings mouse_bindings;
|
||||
bool legacy_paste;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
#ifdef HAVE_V4L2
|
||||
# include <libavdevice/avdevice.h>
|
||||
#endif
|
||||
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "cli.h"
|
||||
#include "options.h"
|
||||
#include "scrcpy.h"
|
||||
#ifdef HAVE_USB
|
||||
# include "usb/scrcpy_otg.h"
|
||||
#endif
|
||||
#include "usb/scrcpy_otg.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
@@ -57,8 +57,8 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc,
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if (!sc_mouse_capture_is_active(mc)) {
|
||||
// The mouse will be captured on SDL_EVENT_MOUSE_BUTTON_UP, so
|
||||
// consume the event
|
||||
// The mouse will be captured on SDL_MOUSEBUTTONUP, so consume
|
||||
// the event
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -16,6 +16,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.camera_id = NULL,
|
||||
.camera_size = NULL,
|
||||
.camera_ar = NULL,
|
||||
.camera_zoom = NULL,
|
||||
.camera_fps = 0,
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.video_codec = SC_CODEC_H264,
|
||||
@@ -113,6 +114,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.angle = NULL,
|
||||
.vd_destroy_content = true,
|
||||
.vd_system_decorations = true,
|
||||
.camera_torch = false,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
||||
@@ -241,6 +241,7 @@ struct scrcpy_options {
|
||||
const char *camera_id;
|
||||
const char *camera_size;
|
||||
const char *camera_ar;
|
||||
const char *camera_zoom;
|
||||
uint16_t camera_fps;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
@@ -327,6 +328,7 @@ struct scrcpy_options {
|
||||
const char *start_app;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
bool camera_torch;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
||||
@@ -541,7 +541,10 @@ sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) {
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
AVCodecContext *ctx) {
|
||||
AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
(void) session;
|
||||
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->video_init);
|
||||
@@ -635,7 +638,10 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
AVCodecContext *ctx) {
|
||||
AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
(void) session;
|
||||
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
|
||||
@@ -355,7 +355,7 @@ scrcpy_generate_scid(void) {
|
||||
|
||||
static void
|
||||
init_sdl_gamepads(void) {
|
||||
// Trigger a SDL_EVENT_GAMEPAD_ADDED event for all gamepads already
|
||||
// Trigger a SDL_CONTROLLERDEVICEADDED event for all gamepads already
|
||||
// connected
|
||||
int count;
|
||||
SDL_JoystickID *joysticks = SDL_GetJoysticks(&count);
|
||||
@@ -469,6 +469,8 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.power_on = options->power_on,
|
||||
.kill_adb_on_close = options->kill_adb_on_close,
|
||||
.camera_high_speed = options->camera_high_speed,
|
||||
.camera_torch = options->camera_torch,
|
||||
.camera_zoom = options->camera_zoom,
|
||||
.vd_destroy_content = options->vd_destroy_content,
|
||||
.vd_system_decorations = options->vd_system_decorations,
|
||||
.list = options->list,
|
||||
@@ -802,6 +804,7 @@ aoa_complete:
|
||||
|
||||
struct sc_screen_params screen_params = {
|
||||
.video = options->video_playback,
|
||||
.camera = options->video_source == SC_VIDEO_SOURCE_CAMERA,
|
||||
.controller = controller,
|
||||
.fp = fp,
|
||||
.kp = kp,
|
||||
|
||||
@@ -27,11 +27,13 @@ get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
|
||||
return oriented_size;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
is_windowed(struct sc_screen *screen) {
|
||||
return !(SDL_GetWindowFlags(screen->window) & (SDL_WINDOW_FULLSCREEN
|
||||
| SDL_WINDOW_MINIMIZED
|
||||
| SDL_WINDOW_MAXIMIZED));
|
||||
// set the window size to be applied when fullscreen is disabled
|
||||
static inline void
|
||||
assert_not_fullscreen(struct sc_screen *screen) {
|
||||
(void) screen;
|
||||
assert(!screen->fullscreen);
|
||||
assert(!screen->maximized);
|
||||
assert(!screen->minimized);
|
||||
}
|
||||
|
||||
// get the preferred display bounds (i.e. the screen bounds with some margins)
|
||||
@@ -39,11 +41,6 @@ static bool
|
||||
get_preferred_display_bounds(struct sc_size *bounds) {
|
||||
SDL_Rect rect;
|
||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||
if (!display) {
|
||||
LOGW("Could not get primary display: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = SDL_GetDisplayUsableBounds(display, &rect);
|
||||
if (!ok) {
|
||||
LOGW("Could not get display usable bounds: %s", SDL_GetError());
|
||||
@@ -219,7 +216,7 @@ event_watcher(void *data, SDL_Event *event) {
|
||||
struct sc_screen *screen = data;
|
||||
assert(screen->video);
|
||||
|
||||
if (event->type == SDL_EVENT_WINDOW_RESIZED) {
|
||||
if (event->type == SDL_EVENT_WINDOW_EXPOSED) {
|
||||
// In practice, it seems to always be called from the same thread in
|
||||
// that specific case. Anyway, it's just a workaround.
|
||||
sc_screen_render(screen, true);
|
||||
@@ -231,9 +228,11 @@ event_watcher(void *data, SDL_Event *event) {
|
||||
|
||||
static bool
|
||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
(void) ctx;
|
||||
(void) session;
|
||||
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
(void) screen;
|
||||
@@ -295,11 +294,15 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->resize_pending = false;
|
||||
screen->has_frame = false;
|
||||
screen->has_video_window = false;
|
||||
screen->fullscreen = false;
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
screen->paused = false;
|
||||
screen->resume_frame = NULL;
|
||||
screen->orientation = SC_ORIENTATION_0;
|
||||
|
||||
screen->video = params->video;
|
||||
screen->camera = params->camera;
|
||||
|
||||
screen->req.x = params->window_x;
|
||||
screen->req.y = params->window_y;
|
||||
@@ -410,6 +413,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
.kp = params->kp,
|
||||
.mp = params->mp,
|
||||
.gp = params->gp,
|
||||
.camera = params->camera,
|
||||
.mouse_bindings = params->mouse_bindings,
|
||||
.legacy_paste = params->legacy_paste,
|
||||
.clipboard_autosync = params->clipboard_autosync,
|
||||
@@ -477,7 +481,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
|
||||
get_initial_optimal_size(screen->content_size, screen->req.width,
|
||||
screen->req.height);
|
||||
|
||||
assert(is_windowed(screen));
|
||||
assert_not_fullscreen(screen);
|
||||
sc_sdl_set_window_size(screen->window, window_size);
|
||||
sc_sdl_set_window_position(screen->window, position);
|
||||
|
||||
@@ -533,7 +537,7 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
|
||||
/ old_content_size.height,
|
||||
};
|
||||
target_size = get_optimal_size(target_size, new_content_size, true);
|
||||
assert(is_windowed(screen));
|
||||
assert_not_fullscreen(screen);
|
||||
sc_sdl_set_window_size(screen->window, target_size);
|
||||
}
|
||||
|
||||
@@ -541,7 +545,7 @@ static void
|
||||
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
|
||||
assert(screen->video);
|
||||
|
||||
if (is_windowed(screen)) {
|
||||
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
|
||||
resize_for_content(screen, screen->content_size, new_content_size);
|
||||
} else if (!screen->resize_pending) {
|
||||
// Store the windowed size to be able to compute the optimal size once
|
||||
@@ -557,7 +561,9 @@ static void
|
||||
apply_pending_resize(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
assert(is_windowed(screen));
|
||||
assert(!screen->fullscreen);
|
||||
assert(!screen->maximized);
|
||||
assert(!screen->minimized);
|
||||
if (screen->resize_pending) {
|
||||
resize_for_content(screen, screen->windowed_content_size,
|
||||
screen->content_size);
|
||||
@@ -705,23 +711,26 @@ void
|
||||
sc_screen_toggle_fullscreen(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
bool req_fullscreen =
|
||||
!(SDL_GetWindowFlags(screen->window) & SDL_WINDOW_FULLSCREEN);
|
||||
|
||||
bool ok = SDL_SetWindowFullscreen(screen->window, req_fullscreen);
|
||||
bool ok = SDL_SetWindowFullscreen(screen->window, !screen->fullscreen);
|
||||
if (!ok) {
|
||||
LOGW("Could not switch fullscreen mode: %s", SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
LOGD("Requested %s mode", req_fullscreen ? "fullscreen" : "windowed");
|
||||
screen->fullscreen = !screen->fullscreen;
|
||||
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
|
||||
apply_pending_resize(screen);
|
||||
}
|
||||
|
||||
LOGD("Switched to %s mode", screen->fullscreen ? "fullscreen" : "windowed");
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_resize_to_fit(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (!is_windowed(screen)) {
|
||||
if (screen->fullscreen || screen->maximized || screen->minimized) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -750,10 +759,15 @@ void
|
||||
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
|
||||
assert(screen->video);
|
||||
|
||||
if (!is_windowed(screen)) {
|
||||
if (screen->fullscreen || screen->minimized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen->maximized) {
|
||||
sc_sdl_restore_window(screen->window);
|
||||
screen->maximized = false;
|
||||
}
|
||||
|
||||
struct sc_size content_size = screen->content_size;
|
||||
sc_sdl_set_window_size(screen->window, content_size);
|
||||
LOGD("Resized to pixel-perfect: %ux%u", content_size.width,
|
||||
@@ -785,20 +799,24 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_MAXIMIZED:
|
||||
screen->maximized = true;
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_MINIMIZED:
|
||||
screen->minimized = true;
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_RESTORED:
|
||||
if (screen->has_video_window && is_windowed(screen)) {
|
||||
apply_pending_resize(screen);
|
||||
sc_screen_render(screen, true);
|
||||
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).
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
|
||||
LOGD("Switched to fullscreen mode");
|
||||
assert(screen->has_video_window);
|
||||
return true;
|
||||
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
|
||||
LOGD("Switched to windowed mode");
|
||||
assert(screen->has_video_window);
|
||||
if (is_windowed(screen)) {
|
||||
screen->maximized = false;
|
||||
screen->minimized = false;
|
||||
if (screen->has_video_window) {
|
||||
apply_pending_resize(screen);
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ struct sc_screen {
|
||||
#endif
|
||||
|
||||
bool video;
|
||||
bool camera;
|
||||
|
||||
struct sc_display display;
|
||||
struct sc_input_manager im;
|
||||
@@ -62,6 +63,9 @@ struct sc_screen {
|
||||
struct SDL_Rect rect;
|
||||
bool has_frame;
|
||||
bool has_video_window;
|
||||
bool fullscreen;
|
||||
bool maximized;
|
||||
bool minimized;
|
||||
|
||||
AVFrame *frame;
|
||||
|
||||
@@ -71,6 +75,7 @@ struct sc_screen {
|
||||
|
||||
struct sc_screen_params {
|
||||
bool video;
|
||||
bool camera;
|
||||
|
||||
struct sc_controller *controller;
|
||||
struct sc_file_pusher *fp;
|
||||
|
||||
@@ -357,6 +357,13 @@ execute_server(struct sc_server *server,
|
||||
if (params->camera_high_speed) {
|
||||
ADD_PARAM("camera_high_speed=true");
|
||||
}
|
||||
if (params->camera_torch) {
|
||||
ADD_PARAM("camera_torch=true");
|
||||
}
|
||||
if (params->camera_zoom) {
|
||||
VALIDATE_STRING(params->camera_zoom);
|
||||
ADD_PARAM("camera_zoom=%s", params->camera_zoom);
|
||||
}
|
||||
if (params->show_touches) {
|
||||
ADD_PARAM("show_touches=true");
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ struct sc_server_params {
|
||||
const char *camera_id;
|
||||
const char *camera_size;
|
||||
const char *camera_ar;
|
||||
const char *camera_zoom;
|
||||
uint16_t camera_fps;
|
||||
struct sc_port_range port_range;
|
||||
uint32_t tunnel_host;
|
||||
@@ -68,6 +69,7 @@ struct sc_server_params {
|
||||
bool power_on;
|
||||
bool kill_adb_on_close;
|
||||
bool camera_high_speed;
|
||||
bool camera_torch;
|
||||
bool vd_destroy_content;
|
||||
bool vd_system_decorations;
|
||||
uint8_t list;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
#include "trait/packet_sink.h"
|
||||
|
||||
/**
|
||||
* Frame sink trait.
|
||||
*
|
||||
@@ -17,9 +19,16 @@ struct sc_frame_sink {
|
||||
|
||||
struct sc_frame_sink_ops {
|
||||
/* The codec context is valid until the sink is closed */
|
||||
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
|
||||
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session);
|
||||
void (*close)(struct sc_frame_sink *sink);
|
||||
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
||||
|
||||
/**
|
||||
* Optional callback to be notified of a new stream session.
|
||||
*/
|
||||
bool (*push_session)(struct sc_frame_sink *sink,
|
||||
const struct sc_stream_session *session);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -27,11 +27,12 @@ sc_frame_source_sinks_close_firsts(struct sc_frame_source *source,
|
||||
|
||||
bool
|
||||
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
||||
const AVCodecContext *ctx) {
|
||||
const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(source->sink_count);
|
||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = source->sinks[i];
|
||||
if (!sink->ops->open(sink, ctx)) {
|
||||
if (!sink->ops->open(sink, ctx, session)) {
|
||||
sc_frame_source_sinks_close_firsts(source, i);
|
||||
return false;
|
||||
}
|
||||
@@ -59,3 +60,18 @@ sc_frame_source_sinks_push(struct sc_frame_source *source,
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_frame_source_sinks_push_session(struct sc_frame_source *source,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(source->sink_count);
|
||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = source->sinks[i];
|
||||
if (sink->ops->push_session &&
|
||||
!sink->ops->push_session(sink, session)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ sc_frame_source_add_sink(struct sc_frame_source *source,
|
||||
|
||||
bool
|
||||
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
||||
const AVCodecContext *ctx);
|
||||
const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session);
|
||||
|
||||
void
|
||||
sc_frame_source_sinks_close(struct sc_frame_source *source);
|
||||
@@ -37,4 +38,8 @@ bool
|
||||
sc_frame_source_sinks_push(struct sc_frame_source *source,
|
||||
const AVFrame *frame);
|
||||
|
||||
bool
|
||||
sc_frame_source_sinks_push_session(struct sc_frame_source *source,
|
||||
const struct sc_stream_session *session);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,12 +15,28 @@ struct sc_packet_sink {
|
||||
const struct sc_packet_sink_ops *ops;
|
||||
};
|
||||
|
||||
struct sc_stream_session_video {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
};
|
||||
|
||||
struct sc_stream_session {
|
||||
struct sc_stream_session_video video;
|
||||
};
|
||||
|
||||
struct sc_packet_sink_ops {
|
||||
/* The codec context is valid until the sink is closed */
|
||||
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
|
||||
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session);
|
||||
void (*close)(struct sc_packet_sink *sink);
|
||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||
|
||||
/**
|
||||
* Optional callback to be notified of a new stream session.
|
||||
*/
|
||||
bool (*push_session)(struct sc_packet_sink *sink,
|
||||
const struct sc_stream_session *session);
|
||||
|
||||
/*/
|
||||
* Called when the input stream has been disabled at runtime.
|
||||
*
|
||||
|
||||
@@ -27,11 +27,12 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||
AVCodecContext *ctx) {
|
||||
AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(source->sink_count);
|
||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = source->sinks[i];
|
||||
if (!sink->ops->open(sink, ctx)) {
|
||||
if (!sink->ops->open(sink, ctx, session)) {
|
||||
sc_packet_source_sinks_close_firsts(source, i);
|
||||
return false;
|
||||
}
|
||||
@@ -60,6 +61,20 @@ sc_packet_source_sinks_push(struct sc_packet_source *source,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_push_session(struct sc_packet_source *source,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(source->sink_count);
|
||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
||||
struct sc_packet_sink *sink = source->sinks[i];
|
||||
if (!sink->ops->push_session(sink, session)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_packet_source_sinks_disable(struct sc_packet_source *source) {
|
||||
assert(source->sink_count);
|
||||
|
||||
@@ -28,7 +28,8 @@ sc_packet_source_add_sink(struct sc_packet_source *source,
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||
AVCodecContext *ctx);
|
||||
AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session);
|
||||
|
||||
void
|
||||
sc_packet_source_sinks_close(struct sc_packet_source *source);
|
||||
@@ -37,6 +38,10 @@ bool
|
||||
sc_packet_source_sinks_push(struct sc_packet_source *source,
|
||||
const AVPacket *packet);
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_push_session(struct sc_packet_source *source,
|
||||
const struct sc_stream_session *session);
|
||||
|
||||
void
|
||||
sc_packet_source_sinks_disable(struct sc_packet_source *source);
|
||||
|
||||
|
||||
@@ -74,9 +74,10 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp,
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(event->gamepad_id);
|
||||
assert(sdl_gamepad);
|
||||
const char *name = SDL_GetGamepadName(sdl_gamepad);
|
||||
SDL_Gamepad * game_controller =
|
||||
SDL_GetGamepadFromID(event->gamepad_id);
|
||||
assert(game_controller);
|
||||
const char *name = SDL_GetGamepadName(game_controller);
|
||||
LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name);
|
||||
|
||||
sc_gamepad_uhid_send_open(gamepad, &hid_open);
|
||||
|
||||
@@ -75,7 +75,7 @@ scrcpy_otg(struct scrcpy_options *options) {
|
||||
|
||||
if (options->gamepad_input_mode != SC_GAMEPAD_INPUT_MODE_DISABLED) {
|
||||
if (!SDL_Init(SDL_INIT_GAMEPAD)) {
|
||||
LOGE("Could not initialize SDL gamepad: %s", SDL_GetError());
|
||||
LOGE("Could not initialize SDL controller: %s", SDL_GetError());
|
||||
// Not fatal, keyboard/mouse should still work
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,16 +192,16 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
|
||||
|
||||
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
|
||||
if (!sdl_gamepad) {
|
||||
LOGW("Could not open gamepad");
|
||||
SDL_Gamepad *gc = SDL_OpenGamepad(event->which);
|
||||
if (!gc) {
|
||||
LOGW("Could not open game controller");
|
||||
return;
|
||||
}
|
||||
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
|
||||
SDL_Joystick *joystick = SDL_GetGamepadJoystick(gc);
|
||||
if (!joystick) {
|
||||
LOGW("Could not get gamepad joystick");
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
LOGW("Could not get controller joystick");
|
||||
SDL_CloseGamepad(gc);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,9 +212,9 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
|
||||
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
SDL_JoystickID id = event->which;
|
||||
|
||||
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
|
||||
if (sdl_gamepad) {
|
||||
SDL_CloseGamepad(sdl_gamepad);
|
||||
SDL_Gamepad *gc = SDL_GetGamepadFromID(id);
|
||||
if (gc) {
|
||||
SDL_CloseGamepad(gc);
|
||||
} else {
|
||||
LOGW("Unknown gamepad device removed");
|
||||
}
|
||||
@@ -258,7 +258,7 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
|
||||
|
||||
struct sc_gamepad_button_event evt = {
|
||||
.gamepad_id = event->which,
|
||||
.action = sc_action_from_sdl_gamepad_button_type(event->type),
|
||||
.action = sc_action_from_sdl_controllerbutton_type(event->type),
|
||||
.button = button,
|
||||
};
|
||||
gp->ops->process_gamepad_button(gp, &evt);
|
||||
|
||||
@@ -130,6 +130,15 @@ sc_sdl_hide_window(SDL_Window *window) {
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sc_sdl_restore_window(SDL_Window *window) {
|
||||
bool ok = SDL_RestoreWindow(window);
|
||||
if (!ok) {
|
||||
LOGE("Could not restore window: %s", SDL_GetError());
|
||||
assert(!"unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
sc_sdl_render_clear(SDL_Renderer *renderer) {
|
||||
bool ok = SDL_RenderClear(renderer);
|
||||
|
||||
@@ -16,12 +16,12 @@ sc_sdl_create_window(const char *title, int64_t x, int64_t y, int64_t width,
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size(SDL_Window *window);
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size_in_pixels(SDL_Window *window);
|
||||
|
||||
void
|
||||
sc_sdl_set_window_size(SDL_Window *window, struct sc_size size);
|
||||
|
||||
struct sc_size
|
||||
sc_sdl_get_window_size_in_pixels(SDL_Window *window);
|
||||
|
||||
struct sc_point
|
||||
sc_sdl_get_window_position(SDL_Window *window);
|
||||
|
||||
@@ -34,6 +34,9 @@ sc_sdl_show_window(SDL_Window *window);
|
||||
void
|
||||
sc_sdl_hide_window(SDL_Window *window);
|
||||
|
||||
void
|
||||
sc_sdl_restore_window(SDL_Window *window);
|
||||
|
||||
bool
|
||||
sc_sdl_render_clear(SDL_Renderer *renderer);
|
||||
|
||||
|
||||
@@ -146,9 +146,11 @@ run_v4l2_sink(void *data) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
(void) ctx;
|
||||
(void) session;
|
||||
|
||||
bool ok = sc_frame_buffer_init(&vs->fb);
|
||||
if (!ok) {
|
||||
@@ -326,9 +328,10 @@ sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
|
||||
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx,
|
||||
const struct sc_stream_session *session) {
|
||||
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||
return sc_v4l2_sink_open(vs, ctx);
|
||||
return sc_v4l2_sink_open(vs, ctx, session);
|
||||
}
|
||||
|
||||
static void
|
||||
|
||||
@@ -446,6 +446,55 @@ static void test_serialize_reset_video(void) {
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_set_torch(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
.camera_set_torch = {
|
||||
.on = true,
|
||||
},
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 2);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_SET_TORCH,
|
||||
0x01, // true
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_zoom_in(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 1);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_IN,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
static void test_serialize_camera_zoom_out(void) {
|
||||
struct sc_control_msg msg = {
|
||||
.type = SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
|
||||
uint8_t buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 1);
|
||||
|
||||
const uint8_t expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_CAMERA_ZOOM_OUT,
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
@@ -470,5 +519,8 @@ int main(int argc, char *argv[]) {
|
||||
test_serialize_open_hard_keyboard();
|
||||
test_serialize_start_app();
|
||||
test_serialize_reset_video();
|
||||
test_serialize_camera_set_torch();
|
||||
test_serialize_camera_zoom_in();
|
||||
test_serialize_camera_zoom_out();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Build with:
|
||||
# podman build -t scrcpy-builder .
|
||||
#
|
||||
# Run with:
|
||||
# podman run \
|
||||
# -v PATH_TO_SCRCPY:/home/debian/scrcpy \
|
||||
# --userns=keep-id \
|
||||
# -it scrcpy-builder \
|
||||
# bash
|
||||
|
||||
FROM debian:trixie
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
sudo wget unzip gcc git pkg-config meson ninja-build cmake \
|
||||
libsdl3-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0-dev openjdk-21-jdk \
|
||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||
|
||||
RUN groupadd -g 1000 debian \
|
||||
&& useradd -m -u 1000 -g 1000 -s /bin/bash debian
|
||||
RUN echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
ENV ANDROID_HOME=/opt/android/sdk
|
||||
RUN mkdir -p "$ANDROID_HOME" \
|
||||
&& chown debian:debian "$ANDROID_HOME"
|
||||
|
||||
USER debian
|
||||
WORKDIR /home/debian
|
||||
|
||||
ENV CMDLINETOOLS_URL=https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
|
||||
ENV CMDLINETOOLS_SHA256=7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee
|
||||
|
||||
RUN wget -q "$CMDLINETOOLS_URL" -O cmdlinetools.zip \
|
||||
&& echo "$CMDLINETOOLS_SHA256 cmdlinetools.zip" | sha256sum -c
|
||||
|
||||
RUN mkdir tmp \
|
||||
&& unzip -q cmdlinetools.zip -d tmp \
|
||||
&& mkdir -p "$ANDROID_HOME/cmdline-tools" \
|
||||
&& mv tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" \
|
||||
&& rmdir tmp \
|
||||
&& rm cmdlinetools.zip
|
||||
# To get the licence hash, run manually: "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses
|
||||
# Build-tools 35 is still needed for the current AGP version
|
||||
RUN mkdir -p "$ANDROID_HOME/licenses" \
|
||||
&& echo 24333f8a63b6825ea9c5514f83c2829b004d1fee > "$ANDROID_HOME/licenses/android-sdk-license" \
|
||||
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-36" "build-tools;35.0.0" "build-tools;36.0.0"
|
||||
|
||||
# For scrcpy build scripts
|
||||
ENV GRADLE_VERSION=8.14.3
|
||||
ENV GRADLE_HOME=/opt/gradle-$GRADLE_VERSION
|
||||
ENV GRADLE_URL=https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip
|
||||
ENV GRADLE_SHA256=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
|
||||
ENV GRADLE=gradle
|
||||
ENV PATH=$GRADLE_HOME/bin:$PATH
|
||||
|
||||
USER root
|
||||
RUN wget -q "$GRADLE_URL" -O gradle.zip \
|
||||
&& echo "$GRADLE_SHA256 gradle.zip" | sha256sum -c
|
||||
RUN unzip -q gradle.zip -d /opt \
|
||||
&& rm gradle.zip
|
||||
|
||||
USER debian
|
||||
|
||||
# Pre-download gradle dependencies for scrcpy
|
||||
RUN mkdir -p /home/debian/fake-scrcpy/app
|
||||
COPY fake.gradle /home/debian/fake-scrcpy/build.gradle
|
||||
COPY fake_app.gradle /home/debian/fake-scrcpy/app/build.gradle
|
||||
RUN echo "include ':app'" > /home/debian/fake-scrcpy/settings.gradle \
|
||||
&& gradle -p /home/debian/fake-scrcpy dependencies androidDependencies --no-daemon \
|
||||
&& rm -rf /home/debian/fake-scrcpy
|
||||
@@ -1,17 +0,0 @@
|
||||
// Fake build.gradle to pre-download gradle dependencies
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Fake app/build.gradle to pre-download gradle dependencies
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
android {
|
||||
buildToolsVersion = "36.0.0"
|
||||
namespace = "com.genymobile.scrcpy"
|
||||
compileSdk 36
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 36
|
||||
}
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '10.12.5'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
24
doc/build.md
24
doc/build.md
@@ -30,13 +30,13 @@ the following files to a directory accessible from your `PATH`:
|
||||
|
||||
It is also available in scrcpy releases.
|
||||
|
||||
The client requires [FFmpeg] and [SDL]. Just follow the instructions.
|
||||
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
|
||||
|
||||
[adb]: https://developer.android.com/studio/command-line/adb.html
|
||||
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
|
||||
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
|
||||
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
|
||||
[SDL]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||
[LibSDL2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +50,10 @@ Install the required packages from your package manager.
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
sudo apt install ffmpeg libsdl3-0 adb libusb-1.0-0
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
|
||||
|
||||
# client build dependencies
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl3-dev \
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0-dev
|
||||
|
||||
@@ -77,7 +77,7 @@ pip3 install meson
|
||||
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
|
||||
|
||||
# client build dependencies
|
||||
sudo dnf install SDL3-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
|
||||
sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make
|
||||
|
||||
# server build dependencies
|
||||
sudo dnf install java-devel
|
||||
@@ -121,7 +121,7 @@ install the required packages:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
pacman -S mingw-w64-x86_64-sdl3 \
|
||||
pacman -S mingw-w64-x86_64-SDL2 \
|
||||
mingw-w64-x86_64-ffmpeg \
|
||||
mingw-w64-x86_64-libusb
|
||||
|
||||
@@ -136,7 +136,7 @@ For a 32 bits version, replace `x86_64` by `i686`:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
pacman -S mingw-w64-i686-sdl3 \
|
||||
pacman -S mingw-w64-i686-SDL2 \
|
||||
mingw-w64-i686-ffmpeg \
|
||||
mingw-w64-i686-libusb
|
||||
|
||||
@@ -162,7 +162,7 @@ Install the packages with [Homebrew]:
|
||||
|
||||
```bash
|
||||
# runtime dependencies
|
||||
brew install sdl3 ffmpeg libusb
|
||||
brew install sdl2 ffmpeg libusb
|
||||
|
||||
# client build dependencies
|
||||
brew install pkg-config meson
|
||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v3.3.4`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e`</sub>
|
||||
- [`scrcpy-server-v3.3.3`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-server-v3.3.3
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
@@ -271,7 +271,7 @@ This installs several files:
|
||||
- `/usr/local/bin/scrcpy` (main app)
|
||||
- `/usr/local/share/scrcpy/scrcpy-server` (server to push to the device)
|
||||
- `/usr/local/share/man/man1/scrcpy.1` (manpage)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/scrcpy.png` (app icon)
|
||||
- `/usr/local/share/icons/hicolor/256x256/apps/icon.png` (app icon)
|
||||
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
||||
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
||||
|
||||
|
||||
@@ -409,12 +409,11 @@ with any client which uses the same protocol.
|
||||
|
||||
For simplicity, some [server-specific options] have been added to produce raw
|
||||
streams easily:
|
||||
- `send_device_meta=false`: disable the device metata (in practice, the device
|
||||
- `send_device_meta=false`: disable device metadata (in practice, the device
|
||||
name) sent on the _first_ socket
|
||||
- `send_frame_meta=false`: disable the 12-byte header for each packet
|
||||
- `send_dummy_byte`: disable the dummy byte sent on forward connections
|
||||
- `send_codec_meta`: disable the codec information (and initial device size for
|
||||
video)
|
||||
- `send_stream_meta`: disable codec and video size metadata
|
||||
- `raw_stream`: disable all the above
|
||||
|
||||
[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329
|
||||
|
||||
10
doc/linux.md
10
doc/linux.md
@@ -6,11 +6,11 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-linux-x86_64-v3.3.4.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `0305d98c06178c67e12427bbf340c436d0d58c9e2a39bf9ffbbf8f54d7ef95a5`</sub>
|
||||
- [`scrcpy-linux-x86_64-v3.3.3.tar.gz`][direct-linux-x86_64] (x86_64)
|
||||
<sub>SHA-256: `9b30e813e8191329ba8025dc80cb0f198fb0a318960a3b5c15395cf675c9c638`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-linux-x86_64-v3.3.4.tar.gz
|
||||
[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-linux-x86_64-v3.3.3.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
@@ -39,8 +39,8 @@ First, you need to install the required packages:
|
||||
|
||||
```bash
|
||||
# for Debian/Ubuntu
|
||||
sudo apt install ffmpeg libsdl3-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl3-dev \
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||
```
|
||||
|
||||
13
doc/macos.md
13
doc/macos.md
@@ -6,14 +6,15 @@
|
||||
|
||||
Download a static build of the [latest release]:
|
||||
|
||||
- [`scrcpy-macos-aarch64-v3.3.4.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `8fef43520405dd523c74e1530ac68febcc5a405ea89712c874936675da8513dd`</sub>
|
||||
- [`scrcpy-macos-x86_64-v3.3.4.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `cf9b3453a33279b6009dfb256b1a84c374bd4c30a71edd74bacab28d72a5d929`</sub>
|
||||
- [`scrcpy-macos-aarch64-v3.3.3.tar.gz`][direct-macos-aarch64] (aarch64)
|
||||
<sub>SHA-256: `b93299468f19ae89ac70f7c1453914c41f1f2bcd31f6ab530038da885c19581f`</sub>
|
||||
|
||||
- [`scrcpy-macos-x86_64-v3.3.3.tar.gz`][direct-macos-x86_64] (x86_64)
|
||||
<sub>SHA-256: `c767fc1d41e4ae26e40558656570962f474739924fd22ee023d8754889ee4366`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-aarch64-v3.3.4.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-macos-x86_64-v3.3.4.tar.gz
|
||||
[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-aarch64-v3.3.3.tar.gz
|
||||
[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-macos-x86_64-v3.3.3.tar.gz
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v3.3.4.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `d8a155b7c180b7ca4cdadd40712b8750b63f3aab48cb5b8a2a39ac2d0d4c5d38`</sub>
|
||||
- [`scrcpy-win32-v3.3.4.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `393f7d5379dabd8aacc41184755c3d0df975cd2861353cb7a8d50e0835e2eb72`</sub>
|
||||
- [`scrcpy-win64-v3.3.3.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `4b458d33d0436688c69875cd267cae6fa8be08aa3c17772edf3a940a3dc4b17e`</sub>
|
||||
- [`scrcpy-win32-v3.3.3.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `e3d43e21c0bd6e070381c390c1e4cccd48a1e71ae73a8c217e6e6b8506598c79`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win64-v3.3.4.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-win32-v3.3.4.zip
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-win64-v3.3.3.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-win32-v3.3.3.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.4/scrcpy-server-v3.3.4
|
||||
PREBUILT_SERVER_SHA256=8588238c9a5a00aa542906b6ec7e6d5541d9ffb9b5d0f6e1bc0e365e2303079e
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.3/scrcpy-server-v3.3.3
|
||||
PREBUILT_SERVER_SHA256=7e70323ba7f259649dd4acce97ac4fefbae8102b2c6d91e2e7be613fd5354be0
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '3.3.4',
|
||||
version: '3.3.3',
|
||||
meson_version: '>= 0.49',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
||||
@@ -7,8 +7,8 @@ android {
|
||||
applicationId = "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 36
|
||||
versionCode 30304
|
||||
versionName "3.3.4"
|
||||
versionCode 30303
|
||||
versionName "3.3.3"
|
||||
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=3.3.4
|
||||
SCRCPY_VERSION_NAME=3.3.3
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-36}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-36.0.0}
|
||||
|
||||
@@ -44,8 +44,10 @@ public class Options {
|
||||
private Size cameraSize;
|
||||
private CameraFacing cameraFacing;
|
||||
private CameraAspectRatio cameraAspectRatio;
|
||||
private float cameraZoom = 1;
|
||||
private int cameraFps;
|
||||
private boolean cameraHighSpeed;
|
||||
private boolean cameraTorch;
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private int screenOffTimeout = -1;
|
||||
@@ -78,7 +80,7 @@ public class Options {
|
||||
private boolean sendDeviceMeta = true; // send device name and size
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||
private boolean sendCodecMeta = true; // write the codec metadata before the stream
|
||||
private boolean sendStreamMeta = true; // write the stream metadata (codec and session)
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
@@ -168,6 +170,10 @@ public class Options {
|
||||
return cameraAspectRatio;
|
||||
}
|
||||
|
||||
public float getCameraZoom() {
|
||||
return cameraZoom;
|
||||
}
|
||||
|
||||
public int getCameraFps() {
|
||||
return cameraFps;
|
||||
}
|
||||
@@ -176,6 +182,10 @@ public class Options {
|
||||
return cameraHighSpeed;
|
||||
}
|
||||
|
||||
public boolean getCameraTorch() {
|
||||
return cameraTorch;
|
||||
}
|
||||
|
||||
public boolean getShowTouches() {
|
||||
return showTouches;
|
||||
}
|
||||
@@ -284,8 +294,8 @@ public class Options {
|
||||
return sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecMeta() {
|
||||
return sendCodecMeta;
|
||||
public boolean getSendStreamMeta() {
|
||||
return sendStreamMeta;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MethodLength")
|
||||
@@ -414,7 +424,6 @@ public class Options {
|
||||
if (!value.isEmpty()) {
|
||||
options.audioEncoder = value;
|
||||
}
|
||||
break;
|
||||
case "power_off_on_close":
|
||||
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||
break;
|
||||
@@ -469,12 +478,20 @@ public class Options {
|
||||
options.cameraAspectRatio = parseCameraAspectRatio(value);
|
||||
}
|
||||
break;
|
||||
case "camera_zoom":
|
||||
if (!value.isEmpty()) {
|
||||
options.cameraZoom = Float.parseFloat(value);
|
||||
}
|
||||
break;
|
||||
case "camera_fps":
|
||||
options.cameraFps = Integer.parseInt(value);
|
||||
break;
|
||||
case "camera_high_speed":
|
||||
options.cameraHighSpeed = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "camera_torch":
|
||||
options.cameraTorch = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "new_display":
|
||||
options.newDisplay = parseNewDisplay(value);
|
||||
break;
|
||||
@@ -501,8 +518,8 @@ public class Options {
|
||||
case "send_dummy_byte":
|
||||
options.sendDummyByte = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "send_codec_meta":
|
||||
options.sendCodecMeta = Boolean.parseBoolean(value);
|
||||
case "send_stream_meta":
|
||||
options.sendStreamMeta = Boolean.parseBoolean(value);
|
||||
break;
|
||||
case "raw_stream":
|
||||
boolean rawStream = Boolean.parseBoolean(value);
|
||||
@@ -510,7 +527,7 @@ public class Options {
|
||||
options.sendDeviceMeta = false;
|
||||
options.sendFrameMeta = false;
|
||||
options.sendDummyByte = false;
|
||||
options.sendCodecMeta = false;
|
||||
options.sendStreamMeta = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.genymobile.scrcpy.video.VideoSource;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.system.Os;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -126,7 +125,7 @@ public final class Server {
|
||||
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
|
||||
}
|
||||
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendStreamMeta(), options.getSendFrameMeta());
|
||||
AsyncProcessor audioRecorder;
|
||||
if (audioCodec == AudioCodec.RAW) {
|
||||
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
|
||||
@@ -137,7 +136,7 @@ public final class Server {
|
||||
}
|
||||
|
||||
if (video) {
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendStreamMeta(),
|
||||
options.getSendFrameMeta());
|
||||
SurfaceCapture surfaceCapture;
|
||||
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
||||
@@ -234,8 +233,6 @@ public final class Server {
|
||||
}
|
||||
});
|
||||
|
||||
dropRootPrivileges();
|
||||
|
||||
prepareMainLooper();
|
||||
|
||||
Options options = Options.parse(args);
|
||||
@@ -276,17 +273,4 @@ public final class Server {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void dropRootPrivileges() {
|
||||
try {
|
||||
if (Os.getuid() == 0) {
|
||||
// Copy-paste does not work with root user
|
||||
// <https://github.com/Genymobile/scrcpy/issues/6224>
|
||||
Os.setuid(2000);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.w("Cannot set UID", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ public final class ControlMessage {
|
||||
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
|
||||
public static final int TYPE_START_APP = 16;
|
||||
public static final int TYPE_RESET_VIDEO = 17;
|
||||
public static final int TYPE_CAMERA_SET_TORCH = 18;
|
||||
public static final int TYPE_CAMERA_ZOOM_IN = 19;
|
||||
public static final int TYPE_CAMERA_ZOOM_OUT = 20;
|
||||
|
||||
public static final long SEQUENCE_INVALID = 0;
|
||||
|
||||
@@ -166,6 +169,13 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createCameraSetTorch(boolean on) {
|
||||
ControlMessage msg = new ControlMessage();
|
||||
msg.type = TYPE_CAMERA_SET_TORCH;
|
||||
msg.on = on;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ public class ControlMessageReader {
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
|
||||
return ControlMessage.createEmpty(type);
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
return parseUhidCreate();
|
||||
@@ -56,6 +58,8 @@ public class ControlMessageReader {
|
||||
return parseUhidDestroy();
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
return parseStartApp();
|
||||
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||
return parseCameraSetTorch();
|
||||
default:
|
||||
throw new ControlProtocolException("Unknown event type: " + type);
|
||||
}
|
||||
@@ -166,6 +170,11 @@ public class ControlMessageReader {
|
||||
return ControlMessage.createStartApp(name);
|
||||
}
|
||||
|
||||
private ControlMessage parseCameraSetTorch() throws IOException {
|
||||
boolean on = dis.readBoolean();
|
||||
return ControlMessage.createCameraSetTorch(on);
|
||||
}
|
||||
|
||||
private Position parsePosition() throws IOException {
|
||||
int x = dis.readInt();
|
||||
int y = dis.readInt();
|
||||
|
||||
@@ -12,7 +12,9 @@ import com.genymobile.scrcpy.device.Position;
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
import com.genymobile.scrcpy.util.LogUtils;
|
||||
import com.genymobile.scrcpy.video.CameraCapture;
|
||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||
import com.genymobile.scrcpy.video.VideoSource;
|
||||
import com.genymobile.scrcpy.video.VirtualDisplayListener;
|
||||
import com.genymobile.scrcpy.wrappers.ClipboardManager;
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
@@ -75,6 +77,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private UhidManager uhidManager;
|
||||
|
||||
private final boolean camera;
|
||||
private final int displayId;
|
||||
private final boolean supportsInputEvents;
|
||||
private final ControlChannel controlChannel;
|
||||
@@ -97,13 +100,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private boolean keepDisplayPowerOff;
|
||||
|
||||
// Used for resetting video encoding on RESET_VIDEO message
|
||||
// Used for resetting video encoding on RESET_VIDEO message or for sending camera controls
|
||||
private SurfaceCapture surfaceCapture;
|
||||
|
||||
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
|
||||
this.displayId = options.getDisplayId();
|
||||
this.camera = options.getVideoSource() == VideoSource.CAMERA;
|
||||
this.controlChannel = controlChannel;
|
||||
this.cleanUp = cleanUp;
|
||||
|
||||
if (this.camera) {
|
||||
// Unused for camera
|
||||
this.displayId = Device.DISPLAY_ID_NONE;
|
||||
this.supportsInputEvents = false;
|
||||
this.sender = null;
|
||||
this.clipboardAutosync = false;
|
||||
this.powerOn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayId = options.getDisplayId();
|
||||
|
||||
this.clipboardAutosync = options.getClipboardAutosync();
|
||||
this.powerOn = options.getPowerOn();
|
||||
initPointers();
|
||||
@@ -201,7 +217,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
|
||||
if (!camera && powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
|
||||
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||
|
||||
// dirty hack
|
||||
@@ -236,7 +252,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
}
|
||||
}, "control-recv");
|
||||
thread.start();
|
||||
sender.start();
|
||||
if (sender != null) {
|
||||
sender.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -244,7 +262,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (thread != null) {
|
||||
thread.interrupt();
|
||||
}
|
||||
sender.stop();
|
||||
if (sender != null) {
|
||||
sender.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,90 +272,122 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
if (thread != null) {
|
||||
thread.join();
|
||||
}
|
||||
sender.join();
|
||||
if (sender != null) {
|
||||
sender.join();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleEvent() throws IOException {
|
||||
ControlMessage msg;
|
||||
try {
|
||||
msg = controlChannel.recv();
|
||||
} catch (ControlProtocolException e) {
|
||||
Ln.e("Control protocol error", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (msg.getType()) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (supportsInputEvents) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
if (supportsInputEvents) {
|
||||
injectText(msg.getText());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
if (supportsInputEvents) {
|
||||
pressBackOrTurnScreenOn(msg.getAction());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
break;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
break;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
getClipboard(msg.getCopyKey());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
break;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
break;
|
||||
case ControlMessage.TYPE_UHID_DESTROY:
|
||||
getUhidManager().close(msg.getId());
|
||||
break;
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
openHardKeyboardSettings();
|
||||
break;
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
break;
|
||||
int type = msg.getType();
|
||||
|
||||
// Events for all sources (display or camera)
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_RESET_VIDEO:
|
||||
resetVideo();
|
||||
break;
|
||||
return true;
|
||||
default:
|
||||
// do nothing
|
||||
// fall through
|
||||
}
|
||||
|
||||
return true;
|
||||
if (!camera) {
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_INJECT_KEYCODE:
|
||||
if (supportsInputEvents) {
|
||||
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_TEXT:
|
||||
if (supportsInputEvents) {
|
||||
injectText(msg.getText());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectTouch(
|
||||
msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
if (supportsInputEvents) {
|
||||
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
|
||||
if (supportsInputEvents) {
|
||||
pressBackOrTurnScreenOn(msg.getAction());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||
Device.expandNotificationPanel();
|
||||
return true;
|
||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||
Device.expandSettingsPanel();
|
||||
return true;
|
||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||
Device.collapsePanels();
|
||||
return true;
|
||||
case ControlMessage.TYPE_GET_CLIPBOARD:
|
||||
getClipboard(msg.getCopyKey());
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||
return true;
|
||||
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||
if (supportsInputEvents) {
|
||||
setDisplayPower(msg.getOn());
|
||||
}
|
||||
return true;
|
||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||
Device.rotateDevice(getActionDisplayId());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_CREATE:
|
||||
getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_INPUT:
|
||||
getUhidManager().writeInput(msg.getId(), msg.getData());
|
||||
return true;
|
||||
case ControlMessage.TYPE_UHID_DESTROY:
|
||||
getUhidManager().close(msg.getId());
|
||||
return true;
|
||||
case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
||||
openHardKeyboardSettings();
|
||||
return true;
|
||||
case ControlMessage.TYPE_START_APP:
|
||||
startAppAsync(msg.getText());
|
||||
return true;
|
||||
default:
|
||||
// fall through
|
||||
}
|
||||
} else {
|
||||
assert surfaceCapture instanceof CameraCapture;
|
||||
CameraCapture cameraCapture = (CameraCapture) surfaceCapture;
|
||||
switch (type) {
|
||||
case ControlMessage.TYPE_CAMERA_SET_TORCH:
|
||||
cameraCapture.setTorchEnabled(msg.getOn());
|
||||
return true;
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_IN:
|
||||
cameraCapture.zoomIn();
|
||||
return true;
|
||||
case ControlMessage.TYPE_CAMERA_ZOOM_OUT:
|
||||
cameraCapture.zoomOut();
|
||||
return true;
|
||||
default:
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
throw new AssertionError("Unexpected message type: " + type);
|
||||
}
|
||||
|
||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||
@@ -751,7 +803,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
||||
private void resetVideo() {
|
||||
if (surfaceCapture != null) {
|
||||
Ln.i("Video capture reset");
|
||||
surfaceCapture.requestInvalidate();
|
||||
surfaceCapture.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@ import java.util.Arrays;
|
||||
|
||||
public final class Streamer {
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
private static final long PACKET_FLAG_SESSION = 1L << 63;
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 62;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 61;
|
||||
|
||||
private final FileDescriptor fd;
|
||||
private final Codec codec;
|
||||
private final boolean sendCodecMeta;
|
||||
private final boolean sendStreamMeta;
|
||||
private final boolean sendFrameMeta;
|
||||
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
@@ -27,7 +28,7 @@ public final class Streamer {
|
||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
|
||||
this.fd = fd;
|
||||
this.codec = codec;
|
||||
this.sendCodecMeta = sendCodecMeta;
|
||||
this.sendStreamMeta = sendCodecMeta;
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ public final class Streamer {
|
||||
}
|
||||
|
||||
public void writeAudioHeader() throws IOException {
|
||||
if (sendCodecMeta) {
|
||||
if (sendStreamMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.flip();
|
||||
@@ -44,12 +45,10 @@ public final class Streamer {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeVideoHeader(Size videoSize) throws IOException {
|
||||
if (sendCodecMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(12);
|
||||
public void writeVideoHeader() throws IOException {
|
||||
if (sendStreamMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.putInt(videoSize.getWidth());
|
||||
buffer.putInt(videoSize.getHeight());
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
@@ -89,6 +88,18 @@ public final class Streamer {
|
||||
writePacket(codecBuffer, pts, config, keyFrame);
|
||||
}
|
||||
|
||||
public void writeSessionMeta(int width, int height) throws IOException {
|
||||
if (sendStreamMeta) {
|
||||
headerBuffer.clear();
|
||||
|
||||
headerBuffer.putInt((int) (PACKET_FLAG_SESSION >> 32)); // Set the first bit to 1
|
||||
headerBuffer.putInt(width);
|
||||
headerBuffer.putInt(height);
|
||||
headerBuffer.flip();
|
||||
IO.writeFully(fd, headerBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException {
|
||||
headerBuffer.clear();
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.media.MediaCodecList;
|
||||
import android.os.Build;
|
||||
import android.util.Range;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -164,7 +165,7 @@ public final class LogUtils {
|
||||
// Capture frame rates for low-FPS mode are the same for every resolution
|
||||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
if (lowFpsRanges != null) {
|
||||
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
||||
String uniqueLowFps = getFormattedUniqueSet(lowFpsRanges);
|
||||
builder.append(", fps=").append(uniqueLowFps);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -172,6 +173,18 @@ public final class LogUtils {
|
||||
Ln.w("Could not get available frame rates for camera " + id, e);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= AndroidVersions.API_30_ANDROID_11) {
|
||||
try {
|
||||
Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
||||
if (zoomRange != null) {
|
||||
String zoom = getFormattedZoomRange(zoomRange);
|
||||
builder.append(", zoom-range=").append(zoom);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Ln.w("Could not get available zoom ranges for camera " + id, e);
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(')');
|
||||
|
||||
if (includeSizes) {
|
||||
@@ -191,7 +204,7 @@ public final class LogUtils {
|
||||
builder.append("\n High speed capture (--camera-high-speed):");
|
||||
for (android.util.Size size : highSpeedSizes) {
|
||||
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
|
||||
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
|
||||
String uniqueHighFps = getFormattedUniqueSet(highFpsRanges);
|
||||
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
|
||||
builder.append(" (fps=").append(uniqueHighFps).append(')');
|
||||
}
|
||||
@@ -205,14 +218,31 @@ public final class LogUtils {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
|
||||
private static String getFormattedUniqueSet(Range<Integer>[] ranges) {
|
||||
SortedSet<Integer> set = new TreeSet<>();
|
||||
for (Range<Integer> range : ranges) {
|
||||
set.add(range.getUpper());
|
||||
}
|
||||
return set;
|
||||
|
||||
StringBuilder builder = new StringBuilder("{");
|
||||
boolean first = true;
|
||||
for (Integer i : set) {
|
||||
if (!first) {
|
||||
builder.append(", ");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
builder.append(i);
|
||||
}
|
||||
builder.append("}");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String getFormattedZoomRange(Range<Float> range) {
|
||||
DecimalFormat format = new DecimalFormat("#.##");
|
||||
return "[" + format.format(range.getLower()) + ", " + format.format(range.getUpper()) + "]";
|
||||
}
|
||||
|
||||
public static String buildAppListMessage() {
|
||||
List<DeviceApp> apps = Device.listApps();
|
||||
|
||||
@@ -36,6 +36,7 @@ import android.view.Surface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -53,6 +54,8 @@ public class CameraCapture extends SurfaceCapture {
|
||||
0, 1, 0, 1, // column 4
|
||||
};
|
||||
|
||||
private static final float ZOOM_FACTOR = 1 + 1 / 16f;
|
||||
|
||||
private final String explicitCameraId;
|
||||
private final CameraFacing cameraFacing;
|
||||
private final Size explicitSize;
|
||||
@@ -63,10 +66,13 @@ public class CameraCapture extends SurfaceCapture {
|
||||
private final Rect crop;
|
||||
private final Orientation captureOrientation;
|
||||
private final float angle;
|
||||
private final boolean initialTorch;
|
||||
private float zoom;
|
||||
|
||||
private String cameraId;
|
||||
private Size captureSize;
|
||||
private Size videoSize; // after OpenGL transforms
|
||||
private Range<Float> zoomRange;
|
||||
|
||||
private AffineMatrix transform;
|
||||
private OpenGLRunner glRunner;
|
||||
@@ -78,6 +84,11 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
private final AtomicBoolean disconnected = new AtomicBoolean();
|
||||
|
||||
// The following fields must be accessed only from the camera thread
|
||||
private boolean started;
|
||||
private CaptureRequest.Builder requestBuilder;
|
||||
private CameraCaptureSession currentSession;
|
||||
|
||||
public CameraCapture(Options options) {
|
||||
this.explicitCameraId = options.getCameraId();
|
||||
this.cameraFacing = options.getCameraFacing();
|
||||
@@ -90,6 +101,8 @@ public class CameraCapture extends SurfaceCapture {
|
||||
this.captureOrientation = options.getCaptureOrientation();
|
||||
assert captureOrientation != null;
|
||||
this.angle = options.getAngle();
|
||||
this.initialTorch = options.getCameraTorch();
|
||||
this.zoom = options.getCameraZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -250,6 +263,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
return ratio.getAspectRatio();
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_28_ANDROID_9)
|
||||
@Override
|
||||
public void start(Surface surface) throws IOException {
|
||||
if (transform != null) {
|
||||
@@ -261,11 +275,67 @@ public class CameraCapture extends SurfaceCapture {
|
||||
surface = glRunner.start(captureSize, videoSize, surface);
|
||||
}
|
||||
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
started = true;
|
||||
});
|
||||
|
||||
Surface captureSurface = surface;
|
||||
OutputConfiguration outputConfig = new OutputConfiguration(captureSurface);
|
||||
List<OutputConfiguration> outputs = Collections.singletonList(outputConfig);
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
assertCameraThread();
|
||||
if (!started) {
|
||||
// Stopped on the encoder thread between the call to start() and this callback
|
||||
return;
|
||||
}
|
||||
|
||||
CameraManager cameraManager = ServiceManager.getCameraManager();
|
||||
try {
|
||||
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
|
||||
zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.w("Could not get camera characteristics");
|
||||
}
|
||||
|
||||
try {
|
||||
requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
requestBuilder.addTarget(captureSurface);
|
||||
|
||||
if (fps > 0) {
|
||||
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
|
||||
}
|
||||
if (initialTorch) {
|
||||
requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
|
||||
}
|
||||
if (zoom != 1) {
|
||||
zoom = clampZoom(zoom);
|
||||
Ln.i("Set camera zoom: " + zoom);
|
||||
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
|
||||
}
|
||||
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(session, request);
|
||||
currentSession = session;
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
Ln.e("Camera configuration error");
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
|
||||
CaptureRequest request = createCaptureRequest(surface);
|
||||
setRepeatingRequest(session, request);
|
||||
} catch (CameraAccessException | InterruptedException e) {
|
||||
cameraDevice.createCaptureSession(sessionConfig);
|
||||
} catch (CameraAccessException e) {
|
||||
stop();
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -273,6 +343,13 @@ public class CameraCapture extends SurfaceCapture {
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
currentSession = null;
|
||||
requestBuilder = null;
|
||||
started = false;
|
||||
});
|
||||
|
||||
if (glRunner != null) {
|
||||
glRunner.stopAndRelease();
|
||||
glRunner = null;
|
||||
@@ -353,46 +430,7 @@ public class CameraCapture extends SurfaceCapture {
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_31_ANDROID_12)
|
||||
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException {
|
||||
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
|
||||
OutputConfiguration outputConfig = new OutputConfiguration(surface);
|
||||
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
|
||||
|
||||
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
|
||||
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
|
||||
@Override
|
||||
public void onConfigured(CameraCaptureSession session) {
|
||||
future.complete(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureFailed(CameraCaptureSession session) {
|
||||
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
|
||||
}
|
||||
});
|
||||
|
||||
camera.createCaptureSession(sessionConfig);
|
||||
|
||||
try {
|
||||
return future.get();
|
||||
} catch (ExecutionException e) {
|
||||
throw (CameraAccessException) e.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException {
|
||||
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
|
||||
requestBuilder.addTarget(surface);
|
||||
|
||||
if (fps > 0) {
|
||||
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
|
||||
}
|
||||
|
||||
return requestBuilder.build();
|
||||
}
|
||||
|
||||
@TargetApi(AndroidVersions.API_31_ANDROID_12)
|
||||
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
|
||||
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException {
|
||||
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
|
||||
@Override
|
||||
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
|
||||
@@ -419,8 +457,61 @@ public class CameraCapture extends SurfaceCapture {
|
||||
return disconnected.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
|
||||
public void setTorchEnabled(boolean enabled) {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
if (currentSession != null && requestBuilder != null) {
|
||||
try {
|
||||
requestBuilder.set(CaptureRequest.FLASH_MODE, enabled ? CaptureRequest.FLASH_MODE_TORCH : CaptureRequest.FLASH_MODE_OFF);
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(currentSession, request);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void zoom(boolean in) {
|
||||
cameraHandler.post(() -> {
|
||||
assertCameraThread();
|
||||
if (currentSession != null && requestBuilder != null) {
|
||||
// Always align to log values
|
||||
double z = Math.round(Math.log(zoom) / Math.log(ZOOM_FACTOR));
|
||||
double dir = in ? 1 : -1;
|
||||
zoom = (float) Math.pow(ZOOM_FACTOR, z + dir);
|
||||
|
||||
try {
|
||||
zoom = clampZoom(zoom);
|
||||
Ln.i("Set camera zoom: " + zoom);
|
||||
requestBuilder.set(CaptureRequest.CONTROL_ZOOM_RATIO, zoom);
|
||||
CaptureRequest request = requestBuilder.build();
|
||||
setRepeatingRequest(currentSession, request);
|
||||
} catch (CameraAccessException e) {
|
||||
Ln.e("Camera error", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void zoomIn() {
|
||||
zoom(true);
|
||||
}
|
||||
|
||||
public void zoomOut() {
|
||||
zoom(false);
|
||||
}
|
||||
|
||||
private float clampZoom(float value) {
|
||||
assertCameraThread();
|
||||
if (zoomRange == null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return zoomRange.clamp(value);
|
||||
}
|
||||
|
||||
private void assertCameraThread() {
|
||||
assert Thread.currentThread() == cameraThread;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +261,4 @@ public class NewDisplayCapture extends SurfaceCapture {
|
||||
int num = size.getMax();
|
||||
return initialDpi * num / den;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +211,4 @@ public class ScreenCapture extends SurfaceCapture {
|
||||
SurfaceControl.closeTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestInvalidate() {
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,9 @@ public abstract class SurfaceCapture {
|
||||
private CaptureListener listener;
|
||||
|
||||
/**
|
||||
* Notify the listener that the capture has been invalidated (for example, because its size changed).
|
||||
* Notify the listener that the capture has been invalidated (for example, because its size changed, or due to a manual user request).
|
||||
*/
|
||||
protected void invalidate() {
|
||||
public void invalidate() {
|
||||
listener.onInvalidated();
|
||||
}
|
||||
|
||||
@@ -86,11 +86,4 @@ public abstract class SurfaceCapture {
|
||||
public boolean isClosed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually request to invalidate (typically a user request).
|
||||
* <p>
|
||||
* The capture implementation is free to ignore the request and do nothing.
|
||||
*/
|
||||
public abstract void requestInvalidate();
|
||||
}
|
||||
|
||||
@@ -71,16 +71,13 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
|
||||
try {
|
||||
boolean alive;
|
||||
boolean headerWritten = false;
|
||||
|
||||
streamer.writeVideoHeader();
|
||||
|
||||
do {
|
||||
reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled
|
||||
capture.prepare();
|
||||
Size size = capture.getSize();
|
||||
if (!headerWritten) {
|
||||
streamer.writeVideoHeader(size);
|
||||
headerWritten = true;
|
||||
}
|
||||
|
||||
format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth());
|
||||
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
|
||||
@@ -107,6 +104,7 @@ public class SurfaceEncoder implements AsyncProcessor {
|
||||
boolean resetRequested = reset.consumeReset();
|
||||
if (!resetRequested) {
|
||||
// If a reset is requested during encode(), it will interrupt the encoding by an EOS
|
||||
streamer.writeSessionMeta(size.getWidth(), size.getHeight());
|
||||
encode(mediaCodec, streamer);
|
||||
}
|
||||
// The capture might have been closed internally (for example if the camera is disconnected)
|
||||
|
||||
@@ -422,6 +422,56 @@ public class ControlMessageReaderTest {
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraSetTorch() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_SET_TORCH);
|
||||
dos.writeBoolean(true);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_SET_TORCH, event.getType());
|
||||
Assert.assertTrue(event.getOn());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraZoomIn() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_IN);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_IN, event.getType());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCameraZoomIn() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(bos);
|
||||
dos.writeByte(ControlMessage.TYPE_CAMERA_ZOOM_OUT);
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||
|
||||
ControlMessage event = reader.read();
|
||||
Assert.assertEquals(ControlMessage.TYPE_CAMERA_ZOOM_OUT, event.getType());
|
||||
|
||||
Assert.assertEquals(-1, bis.read()); // EOS
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiEvents() throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
|
||||
Reference in New Issue
Block a user