Compare commits

...

40 Commits

Author SHA1 Message Date
Romain Vimont
ac82761932 icon-from-filename 2026-01-31 22:14:05 +01:00
Romain Vimont
0f93b7fea1 SCRCPY_ICON_DIR 2026-01-31 22:11:10 +01:00
Romain Vimont
aa2f826a9a sc_file_build_path 2026-01-31 22:11:10 +01:00
Romain Vimont
9fb194c3ad Rename icon.png to scrcpy.png
This makes the icon name consistent everywhere.
2026-01-31 22:11:10 +01:00
Romain Vimont
cf47819c18 sc_texture-in-otg 2026-01-31 22:11:10 +01:00
Romain Vimont
4c32823852 extract-rect-util 2026-01-31 22:11:10 +01:00
Romain Vimont
19c015dd91 do-not-fail-if-icon-fails 2026-01-31 22:11:08 +01:00
Romain Vimont
66dda394d7 rename display to texture 2026-01-31 21:38:32 +01:00
Romain Vimont
123df8e20e set_texture_from_frame 2026-01-31 21:37:43 +01:00
Romain Vimont
ffc7847608 set_texture_from_surface 2026-01-31 21:37:40 +01:00
Romain Vimont
b8d25586bf frect 2026-01-31 21:33:48 +01:00
Romain Vimont
73b503427c render icon using common code can_upscale 2026-01-31 21:33:48 +01:00
Romain Vimont
57acb665c0 update_rect 2026-01-31 21:33:48 +01:00
Romain Vimont
665c586d76 render_output_size 2026-01-31 21:33:48 +01:00
Romain Vimont
b5580dd2e1 render_from_screen 2026-01-31 21:33:48 +01:00
Romain Vimont
71305012a0 fix_macos 2026-01-31 21:33:48 +01:00
Romain Vimont
9ca85aadef Move renderer from sc_display to sc_screen
Make sc_screen the owner of both the SDL window and the SDL renderer.
This is the first step toward limiting the role of sc_display to texture
management.
2026-01-31 21:33:48 +01:00
Romain Vimont
3b13a68c7a Simplify texture failure handling
When the scrcpy window is minimized on Windows with D3D9, texture
creation and updates fail.

As a workaround, a mechanism was implemented to reattempt applying the
requested changes.

Since SDL3 defaults to the D3D11 backend, remove this workaround,
which adds a lot of complexity for a backend that should almost never
be used.

However, do not close scrcpy when texture creation or updates fail; only
the frame should be lost.

Refs SDL/#7651 <https://github.com/libsdl-org/SDL/issues/7651>
Refs #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
Refs 6298ef095f
2026-01-31 21:33:48 +01:00
Romain Vimont
362df5cc84 fix_window_condition 2026-01-31 21:33:40 +01:00
Romain Vimont
373b366fa5 Do not attempt to increase audio thread priority
In practice, this always fails for non-privileged processes.
2026-01-17 21:30:19 +01:00
Romain Vimont
5607f12f2a Document camera torch and zoom features
PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:41:09 +01:00
Tommie
8ac04d39f4 Add shortcuts to change the camera zoom
MOD+up and MOD+zoom change the camera zoom.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:40:58 +01:00
Tommie
af355804ef Add option to specify the camera zoom
Add --camera-zoom to specify the camera zoom.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:37:42 +01:00
Romain Vimont
1018f3e9be Display fps set with '{}'
Replace "fps=[15, 30, 60]" with "fps={15, 30, 60}".

The default toString() implementation for a SortedSet uses '[]', but
it is more correct to use '{}' to denote a set.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:36:33 +01:00
Romain Vimont
48fcfdd104 Add shortcuts to switch the camera torch
MOD+t turns on the camera torch, MOD+Shift+t turns it off.

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

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-13 20:35:52 +01:00
Tommie
553813e97d Add option to turn on the camera torch
Add --camera-torch to turn on the camera torch when the camera starts.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2026-01-13 20:32:06 +01:00
Romain Vimont
968f178205 Simplify camera startup code
Avoid multiple back-and-forths between the caller thread and the camera
thread.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:28:32 +01:00
Romain Vimont
1b13d0a22d Enable "reset video" shortcut for camera
Make the existing "reset video" feature (MOD+Shift+R) also work for a
camera video source.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:27:53 +01:00
Romain Vimont
078565d40b Enable controls for camera video source
This will allow the implementation of camera-specific shortcuts.

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

Co-authored-by: Tommie <teh420@gmail.com>
2026-01-13 20:27:33 +01:00
Romain Vimont
cc7c07d4a3 Report control protocol errors
All IOExceptions were ignored to avoid an error on close, but protocol
exceptions must be reported.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:18:36 +01:00
Romain Vimont
24b46f11a5 Throw error on unexpected control message type
PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:15:37 +01:00
Romain Vimont
f348a1f307 Group control-event shortcuts
Group together the shortcuts that trigger control events sent to the
device.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:11:16 +01:00
Romain Vimont
f8846aa76d Simplify capture invalidation
Remove the unnecessary requestInvalidate() indirection. Use a single
invalidate() method instead.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:10:30 +01:00
Romain Vimont
1e51d2c83f Move precondition checks for input events
Always call the appropriate method responsible for handling the input
event, which can then decide to do nothing.

PR #6243 <https://github.com/Genymobile/scrcpy/pull/6243>
2026-01-13 20:08:40 +01:00
Romain Vimont
826076f753 Detect frame size mismatch in decoder
Warn if the size of a decoded video frame does not match the session
metadata.

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
c36433999c Properly handle session packets in delay_buffer
The delay buffer must forward the session packets while preserving
their order relative to media packets.

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
78cba1b7c2 Add session metadata for the video stream
Introduce a new packet type, a "session" packet, containing metadata
about the encoding session. It is used only for the video stream and
currently includes the video resolution.

For illustration, here is a sequence of packets on the video stream:

                                        device rotation
                                        v
    CODEC | SESSION | MEDIA | MEDIA | … | SESSION | MEDIA | MEDIA | …
           1920x1080 <-----------------> 1080x1920 <------------------
                      encoding session 1            encoding session 2

This metadata is not strictly necessary, since the video resolution can
be determined after decoding. However, it allows detection of cases
where the encoder does not respect the requested size (and logs a
warning), even without decoding (e.g., when there is no video playback).

Additional metadata could be added later if necessary, for example the
actual device rotation.

Refs #5918 <https://github.com/Genymobile/scrcpy/pull/5918>
Refs #5894 <https://github.com/Genymobile/scrcpy/pull/5894>
PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>

Co-authored-by: gz0119 <liyong2@4399.com>
2026-01-10 11:53:22 +01:00
Romain Vimont
1015b42e53 Rename "codec meta" to "stream meta"
The stream metadata will contain both:
 - the codec ID at the start of the stream
 - the session metadata (video width and height) at the start of each
   "session" (typically on rotation)

PR #6159 <https://github.com/Genymobile/scrcpy/pull/6159>
2026-01-10 11:53:22 +01:00
Romain Vimont
f4cc07da24 Keep the window hidden during initialization
This prevents blinking during display initialization.
2026-01-09 19:08:04 +01:00
Romain Vimont
fde02a7dfa Fix segfault with --no-video
Do not call SDL_RectToFRect() if geometry is NULL.

Bug introduced by 02989249f6.
2026-01-09 19:06:08 +01:00
67 changed files with 1841 additions and 1115 deletions

View File

@@ -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 \

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -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]'

View File

@@ -15,7 +15,6 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/events.c',
'src/icon.c',
'src/file_pusher.c',
@@ -33,6 +32,7 @@ src = [
'src/scrcpy.c',
'src/screen.c',
'src/server.c',
'src/texture.c',
'src/version.c',
'src/hid/hid_gamepad.c',
'src/hid/hid_keyboard.c',
@@ -57,6 +57,7 @@ src = [
'src/util/process.c',
'src/util/process_intr.c',
'src/util/rand.c',
'src/util/rect.c',
'src/util/sdl.c',
'src/util/strbuf.c',
'src/util/str.c',
@@ -191,8 +192,7 @@ executable('scrcpy', src,
datadir = get_option('datadir') # by default 'share'
install_man('scrcpy.1')
install_data('data/icon.png',
rename: 'scrcpy.png',
install_data('data/scrcpy.png',
install_dir: datadir / 'icons/hicolor/256x256/apps')
install_data('data/zsh-completion/_scrcpy',
install_dir: datadir / 'zsh/site-functions')
@@ -283,6 +283,6 @@ endif
if meson.version().version_compare('>= 0.58.0')
devenv = environment()
devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png')
devenv.set('SCRCPY_ICON_DIR', meson.current_source_dir() / 'data')
meson.add_devenv(devenv)
endif

View File

@@ -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,22 @@ Install APK from computer
.B Drag & drop non-APK file
Push file to device (see \fB\-\-push\-target\fR)
.TP
.B MOD+t
Turn on the camera torch (camera mode only)
.TP
.B MOD+Shift+t
Turn off the camera torch (camera mode only)
.TP
.B MOD+Up
Zoom camera in (camera mode only)
.TP
.B MOD+Down
Zoom camera out (camera mode only)
.SH Environment variables
@@ -827,8 +851,8 @@ Path to adb.
Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
.TP
.B SCRCPY_ICON_PATH
Path to the program icon.
.B SCRCPY_ICON_DIR
Path to the icon directory.
.TP
.B SCRCPY_SERVER_PATH

View File

@@ -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
@@ -117,14 +120,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
ap->device = SDL_GetAudioStreamDevice(ap->stream);
assert(ap->device);
// The thread calling open() is the thread calling push(), which fills the
// audio buffer consumed by the SDL audio thread.
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
if (!ok) {
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH);
(void) ok; // We don't care if it worked, at least we tried
}
ok = SDL_ResumeAudioDevice(ap->device);
if (!ok) {
LOGE("Could not resume audio device: %s", SDL_GetError());

View File

@@ -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,22 @@ static const struct sc_shortcut shortcuts[] = {
.shortcuts = { "Drag & drop non-APK file" },
.text = "Push file to device (see --push-target)",
},
{
.shortcuts = { "MOD+t" },
.text = "Turn on the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Shift+t" },
.text = "Turn off the camera torch (camera mode only)",
},
{
.shortcuts = { "MOD+Up" },
.text = "Zoom camera in (camera mode only)",
},
{
.shortcuts = { "MOD+Down" },
.text = "Zoom camera out (camera mode only)",
},
};
static const struct sc_envvar envvars[] = {
@@ -1220,8 +1249,8 @@ static const struct sc_envvar envvars[] = {
"--tcpip=<addr>) is specified",
},
{
.name = "SCRCPY_ICON_PATH",
.text = "Path to the program icon",
.name = "SCRCPY_ICON_DIR",
.text = "Path to the icon directory",
},
{
.name = "SCRCPY_SERVER_PATH",
@@ -2780,6 +2809,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 +2963,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 +3141,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

View File

@@ -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;

View File

@@ -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;
};
};

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;

View File

@@ -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;
};

View File

@@ -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;
}
}
}

View File

@@ -1,425 +0,0 @@
#include "display.h"
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <libavutil/pixfmt.h>
#include "util/log.h"
#include "util/sdl.h"
static bool
sc_display_init_novideo_icon(struct sc_display *display,
SDL_Surface *icon_novideo) {
assert(icon_novideo);
bool ok = SDL_SetRenderLogicalPresentation(display->renderer,
icon_novideo->w,
icon_novideo->h,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
if (!ok) {
LOGW("Could not set renderer logical size: %s", SDL_GetError());
// don't fail
}
display->texture = SDL_CreateTextureFromSurface(display->renderer,
icon_novideo);
if (!display->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
return true;
}
bool
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps) {
display->renderer = SDL_CreateRenderer(window, NULL);
if (!display->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
return false;
}
const char *renderer_name = SDL_GetRendererName(display->renderer);
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
display->mipmaps = false;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
display->gl_context = NULL;
#endif
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
display->gl_context = SDL_GL_CreateContext(window);
if (!display->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
SDL_DestroyRenderer(display->renderer);
return false;
}
#endif
struct sc_opengl *gl = &display->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
display->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
display->texture = NULL;
display->pending.flags = 0;
display->pending.frame = NULL;
if (icon_novideo) {
// Without video, set a static scrcpy icon as window content
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
if (!ok) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(display->gl_context);
#endif
SDL_DestroyRenderer(display->renderer);
return false;
}
}
return true;
}
void
sc_display_destroy(struct sc_display *display) {
if (display->pending.frame) {
av_frame_free(&display->pending.frame);
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(display->gl_context);
#endif
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
SDL_DestroyRenderer(display->renderer);
}
static enum SDL_Colorspace
sc_display_to_sdl_color_space(enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool full_range = color_range == AVCOL_RANGE_JPEG;
switch (color_space) {
case AVCOL_SPC_BT709:
case AVCOL_SPC_RGB:
return full_range ? SDL_COLORSPACE_BT709_FULL
: SDL_COLORSPACE_BT709_LIMITED;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
return full_range ? SDL_COLORSPACE_BT601_FULL
: SDL_COLORSPACE_BT601_LIMITED;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
return full_range ? SDL_COLORSPACE_BT2020_FULL
: SDL_COLORSPACE_BT2020_LIMITED;
default:
return SDL_COLORSPACE_JPEG;
}
}
static SDL_Texture *
sc_display_create_texture(struct sc_display *display,
struct sc_size size, enum AVColorSpace color_space,
enum AVColorRange color_range) {
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
enum SDL_Colorspace sdl_color_space =
sc_display_to_sdl_color_space(color_space, color_range);
bool ok =
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_YV12);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
SDL_TEXTUREACCESS_STREAMING);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
size.width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
size.height);
ok &= SDL_SetNumberProperty(props,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
sdl_color_space);
if (!ok) {
LOGE("Could not set texture properties");
SDL_DestroyProperties(props);
return NULL;
}
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
SDL_DestroyProperties(props);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (display->mipmaps) {
struct sc_opengl *gl = &display->gl;
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
if (!props) {
LOGE("Could not get texture properties: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
const char *renderer_name = SDL_GetRendererName(display->renderer);
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
SDL_DestroyProperties(props);
if (!texture_id) {
LOGE("Could not get texture id: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
display->texture_id = texture_id;
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return texture;
}
static inline void
sc_display_set_pending_texture(struct sc_display *display,
struct sc_size size,
enum AVColorRange color_range) {
assert(!display->texture);
display->pending.texture.size = size;
display->pending.texture.color_range = color_range;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_TEXTURE;
}
static bool
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
if (!display->pending.frame) {
display->pending.frame = av_frame_alloc();
if (!display->pending.frame) {
LOG_OOM();
return false;
}
}
av_frame_unref(display->pending.frame);
int r = av_frame_ref(display->pending.frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
return false;
}
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
return true;
}
// Forward declaration
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame);
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_TEXTURE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display,
display->pending.texture.size,
display->pending.texture.color_space,
display->pending.texture.color_range);
if (!display->texture) {
return false;
}
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_TEXTURE;
}
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
bool ok = sc_display_update_texture_internal(display,
display->pending.frame);
if (!ok) {
return false;
}
av_frame_unref(display->pending.frame);
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
}
return true;
}
static bool
sc_display_prepare_texture_internal(struct sc_display *display,
struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
assert(size.width && size.height);
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
display->texture =
sc_display_create_texture(display, size, color_space, color_range);
if (!display->texture) {
return false;
}
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
return true;
}
enum sc_display_result
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool ok = sc_display_prepare_texture_internal(display, size, color_space,
color_range);
if (!ok) {
sc_display_set_pending_texture(display, size, color_range);
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
bool ok = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (!ok) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (display->mipmaps) {
assert(display->texture_id);
struct sc_opengl *gl = &display->gl;
gl->BindTexture(GL_TEXTURE_2D, display->texture_id);
gl->GenerateMipmap(GL_TEXTURE_2D);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return true;
}
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
bool ok = sc_display_update_texture_internal(display, frame);
if (!ok) {
ok = sc_display_set_pending_frame(display, frame);
if (!ok) {
LOGE("Could not set pending frame");
return SC_DISPLAY_RESULT_ERROR;
}
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation) {
sc_sdl_render_clear(display->renderer);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
if (!ok) {
return SC_DISPLAY_RESULT_PENDING;
}
}
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture;
if (orientation == SC_ORIENTATION_0) {
SDL_FRect frect;
SDL_RectToFRect(geometry, &frect);
bool ok = SDL_RenderTexture(renderer, texture, NULL, &frect);
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
SDL_FRect frect;
if (sc_orientation_is_swap(orientation)) {
frect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
frect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
frect.w = geometry->h;
frect.h = geometry->w;
} else {
SDL_RectToFRect(geometry, &frect);
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
bool ok = SDL_RenderTextureRotated(renderer, texture, NULL, &frect,
angle, NULL, flip);
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
}
sc_sdl_render_present(display->renderer);
return SC_DISPLAY_RESULT_OK;
}

View File

@@ -1,69 +0,0 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavutil/frame.h>
#include <SDL3/SDL.h>
#include "coords.h"
#include "opengl.h"
#include "options.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_display {
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
bool mipmaps;
uint32_t texture_id; // only set if mipmaps is enabled
struct {
#define SC_DISPLAY_PENDING_FLAG_TEXTURE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct {
struct sc_size size;
enum AVColorSpace color_space;
enum AVColorRange color_range;
} texture;
AVFrame *frame;
} pending;
};
enum sc_display_result {
SC_DISPLAY_RESULT_OK,
SC_DISPLAY_RESULT_PENDING,
SC_DISPLAY_RESULT_ERROR,
};
bool
sc_display_init(struct sc_display *display, SDL_Window *window,
SDL_Surface *icon_novideo, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
enum sc_display_result
sc_display_prepare_texture(struct sc_display *display, struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range);
enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
enum sc_orientation orientation);
#endif

View File

@@ -14,33 +14,37 @@
#include "config.h"
#include "util/env.h"
#ifdef PORTABLE
# include "util/file.h"
#endif
#include "util/file.h"
#include "util/log.h"
#define SCRCPY_PORTABLE_ICON_FILENAME "icon.png"
#define SCRCPY_DEFAULT_ICON_PATH \
PREFIX "/share/icons/hicolor/256x256/apps/scrcpy.png"
#define SCRCPY_DEFAULT_ICON_DIR PREFIX "/share/icons/hicolor/256x256/apps"
static char *
get_icon_path(void) {
char *icon_path = sc_get_env("SCRCPY_ICON_PATH");
if (icon_path) {
get_icon_path(const char *filename) {
char *icon_path;
char *icon_dir = sc_get_env("SCRCPY_ICON_DIR");
if (icon_dir) {
// if the envvar is set, use it
LOGD("Using SCRCPY_ICON_PATH: %s", icon_path);
icon_path = sc_file_build_path(icon_dir, filename);
free(icon_dir);
if (!icon_path) {
LOG_OOM();
return NULL;
}
LOGD("Using icon from SCRCPY_ICON_DIR: %s", icon_path);
return icon_path;
}
#ifndef PORTABLE
LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH);
icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH);
icon_path = sc_file_build_path(SCRCPY_DEFAULT_ICON_DIR, filename);
if (!icon_path) {
LOG_OOM();
return NULL;
}
LOGD("Using icon: %s", icon_path);
#else
icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME);
icon_path = sc_file_get_local_path(filename);
if (!icon_path) {
LOGE("Could not get icon path");
return NULL;
@@ -177,7 +181,7 @@ to_sdl_pixel_format(enum AVPixelFormat fmt) {
}
static SDL_Surface *
load_from_path(const char *path) {
sc_icon_load_from_full_path(const char *path) {
AVFrame *frame = decode_image(path);
if (!frame) {
return NULL;
@@ -274,19 +278,19 @@ error:
}
SDL_Surface *
scrcpy_icon_load(void) {
char *icon_path = get_icon_path();
sc_icon_load(const char *filename) {
char *icon_path = get_icon_path(filename);
if (!icon_path) {
return NULL;
}
SDL_Surface *icon = load_from_path(icon_path);
SDL_Surface *icon = sc_icon_load_from_full_path(icon_path);
free(icon_path);
return icon;
}
void
scrcpy_icon_destroy(SDL_Surface *icon) {
sc_icon_destroy(SDL_Surface *icon) {
SDL_PropertiesID props = SDL_GetSurfaceProperties(icon);
assert(props);
AVFrame *frame = SDL_GetPointerProperty(props, "sc_frame", NULL);

View File

@@ -5,10 +5,12 @@
#include <SDL3/SDL_surface.h>
#define SC_ICON_FILENAME_SCRCPY "scrcpy.png"
SDL_Surface *
scrcpy_icon_load(void);
sc_icon_load(const char *filename);
void
scrcpy_icon_destroy(SDL_Surface *icon);
sc_icon_destroy(SDL_Surface *icon);
#endif

View File

@@ -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_T:
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;
@@ -790,7 +892,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im,
int32_t x = event->x;
int32_t y = event->y;
sc_screen_hidpi_scale_coords(im->screen, &x, &y);
SDL_Rect *r = &im->screen->rect;
SDL_FRect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
@@ -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,6 +1013,12 @@ 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) {
@@ -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;
@@ -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);
}
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
@@ -946,7 +949,7 @@ aoa_complete:
terminate_event_loop();
LOGD("quit...");
if (options->video_playback) {
if (options->window) {
// Close the window immediately on closing, because screen_destroy()
// may only be called once the video demuxer thread is joined (it may
// take time)

View File

@@ -8,6 +8,7 @@
#include "icon.h"
#include "options.h"
#include "util/log.h"
#include "util/rect.h"
#include "util/sdl.h"
#define DISPLAY_MARGINS 96
@@ -55,16 +56,6 @@ get_preferred_display_bounds(struct sc_size *bounds) {
return true;
}
static bool
is_optimal_size(struct sc_size current_size, struct sc_size content_size) {
// The size is optimal if we can recompute one dimension of the current
// size from the other
return current_size.height == current_size.width * content_size.height
/ content_size.width
|| current_size.width == current_size.height * content_size.width
/ content_size.height;
}
// return the optimal size of the window, with the following constraints:
// - it attempts to keep at least one dimension of the current_size (i.e. it
// crops the black borders)
@@ -90,7 +81,7 @@ get_optimal_size(struct sc_size current_size, struct sc_size content_size,
window_size.height = MIN(current_size.height, display_size.height);
}
if (is_optimal_size(window_size, content_size)) {
if (sc_rect_is_optimal_size(window_size, content_size)) {
return window_size;
}
@@ -145,38 +136,13 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(screen->video);
// Only upscale video frames, not icon
bool can_upscale = screen->video;
struct sc_size content_size = screen->content_size;
// The drawable size is the window size * the HiDPI scale
struct sc_size drawable_size =
sc_sdl_get_window_size_in_pixels(screen->window);
SDL_Rect *rect = &screen->rect;
if (is_optimal_size(drawable_size, content_size)) {
rect->x = 0;
rect->y = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
return;
}
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
if (keep_width) {
rect->x = 0;
rect->w = drawable_size.width;
rect->h = drawable_size.width * content_size.height
/ content_size.width;
rect->y = (drawable_size.height - rect->h) / 2;
} else {
rect->y = 0;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
}
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
sc_rect_get_content_location(render_size, screen->content_size, can_upscale,
&screen->rect);
}
// render the texture to the renderer
@@ -185,23 +151,56 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(screen->video);
assert(screen->has_video_window);
assert(!screen->video || screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation);
(void) res; // any error already logged
}
SDL_Renderer *renderer = screen->renderer;
sc_sdl_render_clear(renderer);
static void
sc_screen_render_novideo(struct sc_screen *screen) {
enum sc_display_result res =
sc_display_render(&screen->display, NULL, SC_ORIENTATION_0);
(void) res; // any error already logged
bool ok = false;
SDL_Texture *texture = screen->tex.texture;
if (!texture) {
LOGW("No texture to render");
goto end;
}
SDL_FRect *geometry = &screen->rect;
enum sc_orientation orientation = screen->orientation;
if (orientation == SC_ORIENTATION_0) {
ok = SDL_RenderTexture(renderer, texture, NULL, geometry);
} else {
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
double angle = 90 * cw_rotation;
const SDL_FRect *dstrect = NULL;
SDL_FRect rect;
if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2.f;
rect.y = geometry->y + (geometry->h - geometry->w) / 2.f;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
dstrect = geometry;
}
SDL_FlipMode flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
ok = SDL_RenderTextureRotated(renderer, texture, NULL, dstrect, angle,
NULL, flip);
}
if (!ok) {
LOGE("Could not render texture: %s", SDL_GetError());
}
end:
sc_sdl_render_present(renderer);
}
#if defined(__APPLE__) || defined(_WIN32)
@@ -231,9 +230,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;
@@ -300,6 +301,7 @@ sc_screen_init(struct sc_screen *screen,
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;
@@ -325,7 +327,8 @@ sc_screen_init(struct sc_screen *screen,
}
}
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
// Always create the window hidden to prevent blinking during initialization
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
@@ -334,8 +337,7 @@ sc_screen_init(struct sc_screen *screen,
}
if (params->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE;
window_flags |= SDL_WINDOW_RESIZABLE;
}
const char *title = params->window_title;
@@ -366,41 +368,79 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
screen->gl_context = NULL;
// starts with "opengl"
const char *renderer_name = SDL_GetRendererName(screen->renderer);
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
bool ok = SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
if (!ok) {
LOGW("Could not set a GL Core Profile Context");
}
LOGD("Creating OpenGL Core Profile context");
screen->gl_context = SDL_GL_CreateContext(screen->window);
if (!screen->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
goto error_destroy_renderer;
}
}
#endif
bool mipmaps = params->video;
ok = sc_texture_init(&screen->tex, screen->renderer, mipmaps);
if (!ok) {
goto error_destroy_renderer;
}
ok = SDL_StartTextInput(screen->window);
if (!ok) {
LOGE("Could not enable text input: %s", SDL_GetError());
goto error_destroy_texture;
}
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
if (icon) {
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
} else if (params->video) {
// just a warning
LOGW("Could not load icon");
} else {
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_window;
}
SDL_Surface *icon_novideo = params->video ? NULL : icon;
bool mipmaps = params->video && params->mipmaps;
ok = sc_display_init(&screen->display, screen->window, icon_novideo,
mipmaps);
if (icon) {
scrcpy_icon_destroy(icon);
}
if (!ok) {
goto error_destroy_window;
if (!params->video) {
screen->content_size.width = icon->w;
screen->content_size.height = icon->h;
ok = sc_texture_set_from_surface(&screen->tex, icon);
if (!ok) {
LOGE("Could not set icon: %s", SDL_GetError());
}
}
sc_icon_destroy(icon);
} else {
// not fatal
LOGE("Could not load icon");
if (!params->video) {
// Make sure the content size is initialized
screen->content_size.width = 256;
screen->content_size.height = 256;
}
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_display;
goto error_destroy_texture;
}
struct sc_input_manager_params im_params = {
@@ -410,6 +450,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,
@@ -443,15 +484,27 @@ sc_screen_init(struct sc_screen *screen,
screen->open = false;
#endif
if (!screen->video && sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
if (!screen->video) {
// Show the window immediately
sc_sdl_show_window(screen->window);
if (sc_screen_is_relative_mode(screen)) {
// Capture mouse immediately if video mirroring is disabled
sc_mouse_capture_set_active(&screen->mc, true);
}
}
return true;
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_texture:
sc_texture_destroy(&screen->tex);
error_destroy_renderer:
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
if (screen->gl_context) {
SDL_GL_DestroyContext(screen->gl_context);
}
#endif
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -513,8 +566,12 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_display_destroy(&screen->display);
sc_texture_destroy(&screen->tex);
av_frame_free(&screen->frame);
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DestroyContext(screen->gl_context);
#endif
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
@@ -597,6 +654,7 @@ sc_screen_apply_frame(struct sc_screen *screen) {
if (!screen->has_frame
|| screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed
screen->frame_size = new_frame_size;
@@ -610,28 +668,12 @@ sc_screen_apply_frame(struct sc_screen *screen) {
screen->has_frame = true;
screen->content_size = new_content_size;
}
enum sc_display_result res =
sc_display_prepare_texture(&screen->display, screen->frame_size,
frame->colorspace, frame->color_range);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
}
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
assert(screen->has_frame);
if (!screen->has_video_window) {
@@ -687,7 +729,10 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
sc_screen_apply_frame(screen);
bool ok = sc_screen_apply_frame(screen);
if (!ok) {
LOGE("Resume frame update failed");
}
}
if (!paused) {
@@ -769,14 +814,11 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGE("Frame update failed\n");
return false;
}
return true;
}
case SDL_EVENT_WINDOW_EXPOSED:
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
if (!screen->video || screen->has_video_window) {
sc_screen_render(screen, true);
}
return true;

View File

@@ -12,16 +12,20 @@
#include "controller.h"
#include "coords.h"
#include "display.h"
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
#include "mouse_capture.h"
#include "options.h"
#include "texture.h"
#include "trait/key_processor.h"
#include "trait/frame_sink.h"
#include "trait/mouse_processor.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_screen {
struct sc_frame_sink frame_sink; // frame sink trait
@@ -30,8 +34,9 @@ struct sc_screen {
#endif
bool video;
bool camera;
struct sc_display display;
struct sc_texture tex;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
@@ -48,6 +53,11 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext gl_context;
#endif
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@@ -59,7 +69,7 @@ struct sc_screen {
// client orientation
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_Rect rect;
struct SDL_FRect rect;
bool has_frame;
bool has_video_window;
@@ -71,6 +81,7 @@ struct sc_screen {
struct sc_screen_params {
bool video;
bool camera;
struct sc_controller *controller;
struct sc_file_pusher *fp;

View File

@@ -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");
}

View File

@@ -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;

227
app/src/texture.c Normal file
View File

@@ -0,0 +1,227 @@
#include "texture.h"
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <libavutil/pixfmt.h>
#include "util/log.h"
bool
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps) {
const char *renderer_name = SDL_GetRendererName(renderer);
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
tex->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
struct sc_opengl *gl = &tex->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (mipmaps) {
bool supports_mipmaps =
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
2, 0 /* OpenGL ES 2.0+ */);
if (supports_mipmaps) {
LOGI("Trilinear filtering enabled");
tex->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
tex->renderer = renderer;
tex->texture = NULL;
return true;
}
void
sc_texture_destroy(struct sc_texture *tex) {
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
}
static enum SDL_Colorspace
sc_texture_to_sdl_color_space(enum AVColorSpace color_space,
enum AVColorRange color_range) {
bool full_range = color_range == AVCOL_RANGE_JPEG;
switch (color_space) {
case AVCOL_SPC_BT709:
case AVCOL_SPC_RGB:
return full_range ? SDL_COLORSPACE_BT709_FULL
: SDL_COLORSPACE_BT709_LIMITED;
case AVCOL_SPC_BT470BG:
case AVCOL_SPC_SMPTE170M:
return full_range ? SDL_COLORSPACE_BT601_FULL
: SDL_COLORSPACE_BT601_LIMITED;
case AVCOL_SPC_BT2020_NCL:
case AVCOL_SPC_BT2020_CL:
return full_range ? SDL_COLORSPACE_BT2020_FULL
: SDL_COLORSPACE_BT2020_LIMITED;
default:
return SDL_COLORSPACE_JPEG;
}
}
static SDL_Texture *
sc_texture_create_frame_texture(struct sc_texture *tex,
struct sc_size size,
enum AVColorSpace color_space,
enum AVColorRange color_range) {
SDL_PropertiesID props = SDL_CreateProperties();
if (!props) {
return NULL;
}
enum SDL_Colorspace sdl_color_space =
sc_texture_to_sdl_color_space(color_space, color_range);
bool ok =
SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
SDL_PIXELFORMAT_YV12);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER,
SDL_TEXTUREACCESS_STREAMING);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
size.width);
ok &= SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
size.height);
ok &= SDL_SetNumberProperty(props,
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
sdl_color_space);
if (!ok) {
LOGE("Could not set texture properties");
SDL_DestroyProperties(props);
return NULL;
}
SDL_Renderer *renderer = tex->renderer;
SDL_Texture *texture = SDL_CreateTextureWithProperties(renderer, props);
SDL_DestroyProperties(props);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (tex->mipmaps) {
struct sc_opengl *gl = &tex->gl;
SDL_PropertiesID props = SDL_GetTextureProperties(texture);
if (!props) {
LOGE("Could not get texture properties: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
const char *renderer_name = SDL_GetRendererName(tex->renderer);
const char *key = !renderer_name || !strcmp(renderer_name, "opengl")
? SDL_PROP_TEXTURE_OPENGL_TEXTURE_NUMBER
: SDL_PROP_TEXTURE_OPENGLES2_TEXTURE_NUMBER;
int64_t texture_id = SDL_GetNumberProperty(props, key, 0);
SDL_DestroyProperties(props);
if (!texture_id) {
LOGE("Could not get texture id: %s", SDL_GetError());
SDL_DestroyTexture(texture);
return NULL;
}
assert(!(texture_id & ~0xFFFFFFFF)); // fits in uint32_t
tex->texture_id = texture_id;
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
// Enable trilinear filtering for downscaling
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
GL_LINEAR_MIPMAP_LINEAR);
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return texture;
}
bool
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame) {
struct sc_size size = {frame->width, frame->height};
assert(size.width && size.height);
if (!tex->texture
|| tex->texture_type != SC_TEXTURE_TYPE_FRAME
|| tex->texture_size.width != size.width
|| tex->texture_size.height != size.height) {
// Incompatible texture, recreate it
enum AVColorSpace color_space = frame->colorspace;
enum AVColorRange color_range = frame->color_range;
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
tex->texture = sc_texture_create_frame_texture(tex, size, color_space,
color_range);
if (!tex->texture) {
return false;
}
tex->texture_size = size;
tex->texture_type = SC_TEXTURE_TYPE_FRAME;
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
}
assert(tex->texture);
assert(tex->texture_type == SC_TEXTURE_TYPE_FRAME);
bool ok = SDL_UpdateYUVTexture(tex->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (!ok) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (tex->mipmaps) {
assert(tex->texture_id);
struct sc_opengl *gl = &tex->gl;
gl->BindTexture(GL_TEXTURE_2D, tex->texture_id);
gl->GenerateMipmap(GL_TEXTURE_2D);
gl->BindTexture(GL_TEXTURE_2D, 0);
}
return true;
}
bool
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface) {
if (tex->texture) {
SDL_DestroyTexture(tex->texture);
}
tex->texture = SDL_CreateTextureFromSurface(tex->renderer, surface);
if (!tex->texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
tex->texture_size.width = surface->w;
tex->texture_size.height = surface->h;
tex->texture_type = SC_TEXTURE_TYPE_ICON;
return true;
}

44
app/src/texture.h Normal file
View File

@@ -0,0 +1,44 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavutil/frame.h>
#include <SDL3/SDL.h>
#include "coords.h"
#include "opengl.h"
enum sc_texture_type {
SC_TEXTURE_TYPE_FRAME,
SC_TEXTURE_TYPE_ICON,
};
struct sc_texture {
SDL_Renderer *renderer; // owned by the caller
SDL_Texture *texture;
// Only valid if texture != NULL
struct sc_size texture_size;
enum sc_texture_type texture_type;
struct sc_opengl gl;
bool mipmaps;
uint32_t texture_id; // only set if mipmaps is enabled
};
bool
sc_texture_init(struct sc_texture *tex, SDL_Renderer *renderer, bool mipmaps);
void
sc_texture_destroy(struct sc_texture *tex);
bool
sc_texture_set_from_frame(struct sc_texture *tex, const AVFrame *frame);
bool
sc_texture_set_from_surface(struct sc_texture *tex, SDL_Surface *surface);
#endif

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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);

View File

@@ -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);

View File

@@ -7,14 +7,23 @@
#include "options.h"
#include "util/acksync.h"
#include "util/log.h"
#include "util/rect.h"
#include "util/sdl.h"
static void
sc_screen_otg_render(struct sc_screen_otg *screen) {
sc_sdl_render_clear(screen->renderer);
if (screen->texture) {
bool ok =
SDL_RenderTexture(screen->renderer, screen->texture, NULL, NULL);
SDL_Texture *texture = screen->tex.texture;
if (texture) {
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
SDL_FRect rect;
sc_rect_get_content_location(render_size, screen->tex.texture_size,
false, &rect);
bool ok = SDL_RenderTexture(screen->renderer, texture, NULL, &rect);
if (!ok) {
LOGW("Could not render texture: %s", SDL_GetError());
}
@@ -60,30 +69,25 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
bool ok = sc_texture_init(&screen->tex, screen->renderer, false);
if (!ok) {
goto error_destroy_renderer;
}
SDL_Surface *icon = sc_icon_load(SC_ICON_FILENAME_SCRCPY);
if (icon) {
bool ok = SDL_SetWindowIcon(screen->window, icon);
if (!ok) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
ok = SDL_SetRenderLogicalPresentation(screen->renderer, icon->w,
icon->h,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
ok = sc_texture_set_from_surface(&screen->tex, icon);
sc_icon_destroy(icon);
if (!ok) {
LOGW("Could not set renderer logical size: %s", SDL_GetError());
// don't fail
}
screen->texture = SDL_CreateTextureFromSurface(screen->renderer, icon);
scrcpy_icon_destroy(icon);
if (!screen->texture) {
goto error_destroy_renderer;
LOGE("Could not set icon: %s", SDL_GetError());
}
} else {
screen->texture = NULL;
LOGW("Could not load icon");
LOGE("Could not load icon");
}
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
@@ -95,19 +99,17 @@ sc_screen_otg_init(struct sc_screen_otg *screen,
return true;
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
return false;
}
void
sc_screen_otg_destroy(struct sc_screen_otg *screen) {
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
sc_texture_destroy(&screen->tex);
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
}

View File

@@ -8,6 +8,7 @@
#include <SDL3/SDL.h>
#include "mouse_capture.h"
#include "texture.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
#include "usb/mouse_aoa.h"
@@ -19,7 +20,7 @@ struct sc_screen_otg {
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_texture tex;
struct sc_mouse_capture mc;
};

View File

@@ -5,6 +5,25 @@
#include "util/log.h"
char *
sc_file_build_path(const char *dir, const char *name) {
size_t dir_len = strlen(dir);
size_t name_len = strlen(name);
size_t len = dir_len + name_len + 2; // +2: '/' and '\0'
char *path = malloc(len);
if (!path) {
LOG_OOM();
return NULL;
}
memcpy(path, dir, dir_len);
path[dir_len] = SC_PATH_SEPARATOR;
// namelen + 1 to copy the final '\0'
memcpy(&path[dir_len + 1], name, name_len + 1);
return path;
}
char *
sc_file_get_local_path(const char *name) {
char *executable_path = sc_file_get_executable_path();
@@ -25,24 +44,9 @@ sc_file_get_local_path(const char *name) {
*p = '\0'; // modify executable_path in place
char *dir = executable_path;
size_t dirlen = strlen(dir);
size_t namelen = strlen(name);
size_t len = dirlen + namelen + 2; // +2: '/' and '\0'
char *file_path = malloc(len);
if (!file_path) {
LOG_OOM();
free(executable_path);
return NULL;
}
memcpy(file_path, dir, dirlen);
file_path[dirlen] = SC_PATH_SEPARATOR;
// namelen + 1 to copy the final '\0'
memcpy(&file_path[dirlen + 1], name, namelen + 1);
char *file_path = sc_file_build_path(dir, name);
free(executable_path);
return file_path;
}

View File

@@ -40,6 +40,15 @@ sc_file_get_executable_path(void);
char *
sc_file_get_local_path(const char *name);
/**
* Return the concatenation of dir, the path separator and the filename.
*
* The result must be freed by the caller using free(). It may return NULL on
* error.
*/
char *
sc_file_build_path(const char *dir, const char *filename);
/**
* Indicate if the file exists and is not a directory
*/

52
app/src/util/rect.c Normal file
View File

@@ -0,0 +1,52 @@
#include "rect.h"
bool
sc_rect_is_optimal_size(struct sc_size current_size,
struct sc_size content_size) {
// The size is optimal if we can recompute one dimension of the current
// size from the other
return current_size.height == current_size.width * content_size.height
/ content_size.width
|| current_size.width == current_size.height * content_size.width
/ content_size.height;
}
// Compute the content location, preserving its aspect ratio
void
sc_rect_get_content_location(struct sc_size render_size,
struct sc_size content_size, bool can_upscale,
SDL_FRect *out) {
if (sc_rect_is_optimal_size(render_size, content_size)) {
out->x = 0;
out->y = 0;
out->w = render_size.width;
out->h = render_size.height;
return;
}
if (!can_upscale && content_size.width <= render_size.width
&& content_size.height <= render_size.height) {
// Center without upscaling
out->x = (render_size.width - content_size.width) / 2.f;
out->y = (render_size.height - content_size.height) / 2.f;
out->w = content_size.width;
out->h = content_size.height;
return;
}
bool keep_width = content_size.width * render_size.height
> content_size.height * render_size.width;
if (keep_width) {
out->x = 0;
out->w = render_size.width;
out->h = (float) render_size.width * content_size.height
/ content_size.width;
out->y = (render_size.height - out->h) / 2.f;
} else {
out->y = 0;
out->h = render_size.height;
out->w = (float) render_size.height * content_size.width
/ content_size.height;
out->x = (render_size.width - out->w) / 2.f;
}
}

24
app/src/util/rect.h Normal file
View File

@@ -0,0 +1,24 @@
#ifndef SC_RECT_H
#define SC_RECT_H
#include "common.h"
#include <SDL3/SDL_rect.h>
#include "coords.h"
// Return whether the size is optimal
//
// It is optimal if it does not require black borders to preserve the aspect
// ratio, with rounding applied at pixel boundaries.
bool
sc_rect_is_optimal_size(struct sc_size current_size,
struct sc_size content_size);
// Compute the content location, preserving its aspect ratio
void
sc_rect_get_content_location(struct sc_size render_size,
struct sc_size content_size, bool can_upscale,
SDL_FRect *out);
#endif

View File

@@ -130,6 +130,25 @@ sc_sdl_hide_window(SDL_Window *window) {
}
}
struct sc_size
sc_sdl_get_render_output_size(SDL_Renderer *renderer) {
int width;
int height;
bool ok = SDL_GetRenderOutputSize(renderer, &width, &height);
if (!ok) {
LOGE("Could not get render output size: %s", SDL_GetError());
LOGE("Please report the error");
// fatal error
abort();
}
struct sc_size size = {
.width = width,
.height = height,
};
return size;
}
bool
sc_sdl_render_clear(SDL_Renderer *renderer) {
bool ok = SDL_RenderClear(renderer);

View File

@@ -34,6 +34,9 @@ sc_sdl_show_window(SDL_Window *window);
void
sc_sdl_hide_window(SDL_Window *window);
struct sc_size
sc_sdl_get_render_output_size(SDL_Renderer *renderer);
bool
sc_sdl_render_clear(SDL_Renderer *renderer);

View File

@@ -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

View File

@@ -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;
}

View File

@@ -165,6 +165,30 @@ scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high
[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
## Torch
The camera torch can be turned on at startup by `--camera-torch`:
```
scrcpy --video-source=camera --camera-torch
```
It can also be turned on and off dynamically with <kbd>MOD</kbd>+<kbd>t</kbd>
and <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>, respectively.
## Zoom
The camera zoom can be set with `--camera-zoom=`:
```bash
scrcpy --video-source=camera --camera-zoom=1.5
```
It can also be adjusted dynamically using <kbd>MOD</kbd>+<kbd></kbd> _(up)_ and
<kbd>MOD</kbd>+<kbd></kbd> _(down)_.
## Webcam
Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera

View File

@@ -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

View File

@@ -58,6 +58,10 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Tilt horizontally (slide with 2 fingers) | <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+_click-and-move_
| Drag & drop APK file | Install APK from computer
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
| Turn on the camera torch (camera mode only) | <kbd>MOD</kbd>+<kbd>t</kbd>
| Turn off the camera torch (camera mode only)| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>t</kbd>
| Zoom camera in (camera mode only) | <kbd>MOD</kbd>+<kbd></kbd> _(up)_
| Zoom camera out (camera mode only) | <kbd>MOD</kbd>+<kbd></kbd> _(down)_
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._

View File

@@ -38,6 +38,6 @@ ninja -C "$LINUX_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$LINUX_BUILD_DIR/dist"
cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/"
cp app/data/icon.png "$LINUX_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$LINUX_BUILD_DIR/dist/"
cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/"

View File

@@ -38,6 +38,6 @@ ninja -C "$MACOS_BUILD_DIR"
# Group intermediate outputs into a 'dist' directory
mkdir -p "$MACOS_BUILD_DIR/dist"
cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/"
cp app/data/icon.png "$MACOS_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$MACOS_BUILD_DIR/dist/"
cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/"

View File

@@ -47,7 +47,7 @@ mkdir -p "$WINXX_BUILD_DIR/dist"
cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/"
cp app/data/icon.png "$WINXX_BUILD_DIR/dist/"
cp app/data/scrcpy.png "$WINXX_BUILD_DIR/dist/"
cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/"
cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/"
cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/"

2
run
View File

@@ -20,6 +20,6 @@ then
exit 1
fi
SCRCPY_ICON_PATH="app/data/icon.png" \
SCRCPY_ICON_DIR="app/data" \
SCRCPY_SERVER_PATH="$BUILDDIR/server/scrcpy-server" \
"$BUILDDIR/app/scrcpy" "$@"

View File

@@ -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")
@@ -469,12 +479,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 +519,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 +528,7 @@ public class Options {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendCodecMeta = false;
options.sendStreamMeta = false;
}
break;
default:

View File

@@ -126,7 +126,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 +137,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) {

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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_30_ANDROID_11)
@Override
public void start(Surface surface) throws IOException {
if (transform != null) {
@@ -261,11 +275,68 @@ 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) {
Ln.i("Turn camera torch on");
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 +344,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 +431,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 +458,63 @@ 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 {
Ln.i("Turn camera torch " + (enabled ? "on" : "off"));
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);
}
}
});
}
@TargetApi(AndroidVersions.API_30_ANDROID_11)
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;
}
}

View File

@@ -261,9 +261,4 @@ public class NewDisplayCapture extends SurfaceCapture {
int num = size.getMax();
return initialDpi * num / den;
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@@ -211,9 +211,4 @@ public class ScreenCapture extends SurfaceCapture {
SurfaceControl.closeTransaction();
}
}
@Override
public void requestInvalidate() {
invalidate();
}
}

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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 testParseCameraZoomOut() 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();