Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Vimont
1ad2edc2d8 Dockerfile 2026-01-08 21:14:06 +01:00
68 changed files with 1634 additions and 1946 deletions

View File

@@ -18,8 +18,6 @@ _scrcpy() {
--camera-fps=
--camera-high-speed
--camera-size=
--camera-torch
--camera-zoom=
--capture-orientation=
--crop=
-d --select-usb
@@ -199,8 +197,6 @@ _scrcpy() {
|--camera-id \
|--camera-fps \
|--camera-size \
|--camera-torch \
|--camera-zoom \
|--crop \
|--display-id \
|--max-fps \

View File

@@ -25,8 +25,6 @@ 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,6 +15,7 @@ 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',
@@ -32,7 +33,6 @@ 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',
@@ -75,7 +75,6 @@ conf.set('_GNU_SOURCE', true)
if host_machine.system() == 'windows'
windows = import('windows')
src += [
'src/util/command.c',
'src/sys/win/file.c',
'src/sys/win/process.c',
windows.compile_resources('scrcpy-windows.rc'),
@@ -105,6 +104,7 @@ if usb_support
'src/usb/keyboard_aoa.c',
'src/usb/mouse_aoa.c',
'src/usb/scrcpy_otg.c',
'src/usb/screen_otg.c',
'src/usb/usb.c',
]
endif
@@ -239,12 +239,6 @@ if get_option('buildtype') == 'debug'
'src/util/strbuf.c',
'src/util/term.c',
]],
['test_command_windows', [
'tests/test_command_windows.c',
'src/util/command.c',
'src/util/str.c',
'src/util/strbuf.c',
]],
['test_control_msg_serialize', [
'tests/test_control_msg_serialize.c',
'src/control_msg.c',

View File

@@ -131,14 +131,6 @@ 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 '@'.
@@ -823,22 +815,6 @@ 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

View File

@@ -331,24 +331,56 @@ sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
bool
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
const char *remote, unsigned flags) {
#ifdef _WIN32
// Windows will parse the string, so the paths must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
if (!local) {
return SC_PROCESS_NONE;
}
remote = sc_str_quote(remote);
if (!remote) {
free((void *) local);
return SC_PROCESS_NONE;
}
#endif
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "push", local, remote);
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32
free((void *) remote);
free((void *) local);
#endif
return process_check_success_intr(intr, pid, "adb push", flags);
}
bool
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
unsigned flags) {
#ifdef _WIN32
// Windows will parse the string, so the local name must be quoted
// (see sys/win/command.c)
local = sc_str_quote(local);
if (!local) {
return SC_PROCESS_NONE;
}
#endif
assert(serial);
const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "install", "-r", local);
sc_pid pid = sc_adb_execute(argv, flags);
#ifdef _WIN32
free((void *) local);
#endif
return process_check_success_intr(intr, pid, "adb install", flags);
}

View File

@@ -47,10 +47,7 @@ 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 struct sc_stream_session *session) {
(void) session;
const AVCodecContext *ctx) {
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
@@ -120,6 +117,14 @@ 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,8 +114,6 @@ enum {
OPT_NO_VD_SYSTEM_DECORATIONS,
OPT_NO_VD_DESTROY_CONTENT,
OPT_DISPLAY_IME_POLICY,
OPT_CAMERA_TORCH,
OPT_CAMERA_ZOOM,
};
struct sc_option {
@@ -315,17 +313,6 @@ 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",
@@ -1220,22 +1207,6 @@ 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[] = {
@@ -2809,12 +2780,6 @@ 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;
@@ -2963,7 +2928,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
#endif
if (opts->control && opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
if (opts->control) {
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;
@@ -3141,10 +3106,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
if (opts->control) {
// 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;
LOGI("Camera video source: control disabled");
opts->control = false;
}
} else if (opts->camera_id
|| opts->camera_ar

View File

@@ -182,17 +182,12 @@ 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:
@@ -323,16 +318,6 @@ 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,9 +43,6 @@ 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 {
@@ -114,9 +111,6 @@ struct sc_control_msg {
struct {
char *name;
} start_app;
struct {
bool on;
} camera_set_torch;
};
};

View File

@@ -10,30 +10,20 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
static bool
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx,
const struct sc_stream_session *session) {
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
decoder->frame = av_frame_alloc();
if (!decoder->frame) {
LOG_OOM();
return false;
}
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx, session)) {
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
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;
}
@@ -71,32 +61,6 @@ 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);
@@ -110,17 +74,9 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
}
static bool
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) {
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
struct sc_decoder *decoder = DOWNCAST(sink);
return sc_decoder_open(decoder, ctx, session);
return sc_decoder_open(decoder, ctx);
}
static void
@@ -136,14 +92,6 @@ 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
@@ -153,7 +101,6 @@ 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,7 +5,6 @@
#include <libavcodec/avcodec.h>
#include "coords.h"
#include "trait/frame_source.h"
#include "trait/packet_sink.h"
@@ -17,9 +16,6 @@ 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,18 +10,16 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
static bool
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) {
sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
dframe->frame = av_frame_alloc();
if (!dframe->frame) {
LOG_OOM();
return false;
}
if (av_frame_ref(dpacket->frame, frame)) {
if (av_frame_ref(dframe->frame, frame)) {
LOG_OOM();
av_frame_free(&dpacket->frame);
av_frame_free(&dframe->frame);
return false;
}
@@ -29,18 +27,9 @@ sc_delayed_packet_init_frame(struct sc_delayed_packet *dpacket,
}
static void
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);
}
sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) {
av_frame_unref(dframe->frame);
av_frame_free(&dframe->frame);
}
static int
@@ -61,52 +50,43 @@ run_buffering(void *data) {
goto stopped;
}
struct sc_delayed_packet dpacket = sc_vecdeque_pop(&db->queue);
struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue);
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);
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 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);
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 stopped = db->stopped;
sc_mutex_unlock(&db->mutex);
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());
#endif
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);
timed_out =
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
}
sc_delayed_packet_destroy(&dpacket);
bool stopped = db->stopped;
sc_mutex_unlock(&db->mutex);
if (stopped) {
sc_delayed_frame_destroy(&dframe);
goto stopped;
}
#ifdef SC_BUFFERING_DEBUG
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);
if (!ok) {
LOGE("Delayed packet could not be pushed, stopping");
LOGE("Delayed frame could not be pushed, stopping");
sc_mutex_lock(&db->mutex);
// Prevent to push any new packet
// Prevent to push any new frame
db->stopped = true;
sc_mutex_unlock(&db->mutex);
goto stopped;
@@ -118,8 +98,8 @@ stopped:
// Flush queue
while (!sc_vecdeque_is_empty(&db->queue)) {
struct sc_delayed_packet *dpacket = sc_vecdeque_popref(&db->queue);
sc_delayed_packet_destroy(dpacket);
struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue);
sc_delayed_frame_destroy(dframe);
}
LOGD("Buffering thread ended");
@@ -129,11 +109,9 @@ stopped:
static bool
sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
const AVCodecContext *ctx) {
struct sc_delay_buffer *db = DOWNCAST(sink);
(void) ctx;
(void) session;
bool ok = sc_mutex_init(&db->mutex);
if (!ok) {
@@ -154,7 +132,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, session)) {
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
goto error_destroy_wait_cond;
}
@@ -218,56 +196,24 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
return sc_frame_source_sinks_push(&db->frame_source, frame);
}
struct sc_delayed_packet *dpacket = sc_vecdeque_push_hole(&db->queue);
if (!dpacket) {
struct sc_delayed_frame dframe;
bool ok = sc_delayed_frame_init(&dframe, frame);
if (!ok) {
sc_mutex_unlock(&db->mutex);
LOG_OOM();
return false;
}
bool ok = sc_delayed_packet_init_frame(dpacket, frame);
#ifdef SC_BUFFERING_DEBUG
dframe.push_date = sc_tick_now();
#endif
ok = sc_vecdeque_push(&db->queue, dframe);
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);
@@ -289,7 +235,6 @@ 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,23 +18,14 @@
// forward declarations
typedef struct AVFrame AVFrame;
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;
};
struct sc_delayed_frame {
AVFrame *frame;
#ifdef SC_BUFFERING_DEBUG
sc_tick push_date;
#endif
};
struct sc_delayed_packet_queue SC_VECDEQUE(struct sc_delayed_packet);
struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame);
struct sc_delay_buffer {
struct sc_frame_source frame_source; // frame source trait
@@ -49,7 +40,7 @@ struct sc_delay_buffer {
sc_cond wait_cond;
struct sc_clock clock;
struct sc_delayed_packet_queue queue;
struct sc_delayed_frame_queue queue;
bool stopped;
};

View File

@@ -11,8 +11,8 @@
#define SC_PACKET_HEADER_SIZE 12
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 62)
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 61)
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63)
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62)
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
@@ -63,75 +63,48 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
return true;
}
static inline bool
sc_demuxer_recv_header(struct sc_demuxer *demuxer,
uint8_t buf[static SC_PACKET_HEADER_SIZE]) {
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) {
// 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.
//
//
// 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:
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
// <-------------> <-----> <-----------------------------...
// 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 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;
}
// byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
// CK...... ........ ........ ........ ........ ........ ........ ........
// ^^<------------------------------------------------------------------->
// || PTS
// | `- key frame
// `-- config packet
static bool
sc_demuxer_is_session(const uint8_t *header) {
return header[0] & 0x80;
}
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 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);
@@ -141,7 +114,7 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, const uint8_t *header,
return false;
}
ssize_t r = net_recv_all(demuxer->socket, packet->data, len);
r = net_recv_all(demuxer->socket, packet->data, len);
if (r < 0 || ((uint32_t) r) < len) {
av_packet_unref(packet);
return false;
@@ -214,28 +187,17 @@ 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) {
bool ok = sc_demuxer_recv_header(demuxer, header);
uint32_t width;
uint32_t height;
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
if (!ok) {
goto finally_free_context;
}
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->width = width;
codec_ctx->height = height;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
} else {
// Hardcoded audio properties
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
@@ -257,8 +219,7 @@ run_demuxer(void *data) {
goto finally_free_context;
}
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx,
session)) {
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
goto finally_free_context;
}
@@ -280,39 +241,27 @@ run_demuxer(void *data) {
}
for (;;) {
bool ok = sc_demuxer_recv_header(demuxer, header);
bool ok = sc_demuxer_recv_packet(demuxer, packet);
if (!ok) {
// end of stream
status = SC_DEMUXER_STATUS_EOS;
break;
}
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 (must_merge_config_packet) {
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (!ok) {
// The sink already logged its concrete error
av_packet_unref(packet);
break;
}
} else {
sc_demuxer_recv_packet(demuxer, header, packet);
}
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;
}
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
av_packet_unref(packet);
if (!ok) {
// The sink already logged its concrete error
break;
}
}

425
app/src/display.c Normal file
View File

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

69
app/src/display.h Normal file
View File

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

@@ -16,6 +16,8 @@
void
sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params) {
// A key/mouse processor may not be present if there is no controller
assert((!params->kp && !params->mp && !params->gp) || params->controller);
// A processor must have ops initialized
assert(!params->kp || params->kp->ops);
assert(!params->mp || params->mp->ops);
@@ -27,7 +29,6 @@ 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;
@@ -51,7 +52,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 && !im->camera);
assert(im->controller && im->kp);
// send DOWN event
struct sc_control_msg msg;
@@ -108,7 +109,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 && !im->camera);
assert(im->controller && im->kp);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
@@ -123,7 +124,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 && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
@@ -135,7 +136,7 @@ expand_notification_panel(struct sc_input_manager *im) {
static void
expand_settings_panel(struct sc_input_manager *im) {
assert(im->controller && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL;
@@ -147,7 +148,7 @@ expand_settings_panel(struct sc_input_manager *im) {
static void
collapse_panels(struct sc_input_manager *im) {
assert(im->controller && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS;
@@ -159,7 +160,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 && !im->camera);
assert(im->controller && im->kp);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_GET_CLIPBOARD;
@@ -176,7 +177,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 && !im->camera);
assert(im->controller && im->kp);
char *text = SDL_GetClipboardText();
if (!text) {
@@ -208,7 +209,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 && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
@@ -235,7 +236,7 @@ switch_fps_counter_state(struct sc_input_manager *im) {
static void
clipboard_paste(struct sc_input_manager *im) {
assert(im->controller && im->kp && !im->camera);
assert(im->controller && im->kp);
char *text = SDL_GetClipboardText();
if (!text) {
@@ -266,7 +267,7 @@ clipboard_paste(struct sc_input_manager *im) {
static void
rotate_device(struct sc_input_manager *im) {
assert(im->controller && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_ROTATE_DEVICE;
@@ -278,7 +279,7 @@ rotate_device(struct sc_input_manager *im) {
static void
open_hard_keyboard_settings(struct sc_input_manager *im) {
assert(im->controller && !im->camera);
assert(im->controller);
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS;
@@ -300,43 +301,6 @@ 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) {
@@ -349,10 +313,6 @@ 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;
@@ -410,9 +370,6 @@ 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;
@@ -446,45 +403,106 @@ 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);
}
return;
} else if (im->kp && !paused) {
// forward repeated events
action_volume_down(im, action);
}
break;
return;
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);
}
return;
} else if (im->kp && !paused) {
// forward repeated events
action_volume_up(im, action);
}
break;
return;
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);
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);
}
}
return;
@@ -508,134 +526,33 @@ sc_input_manager_process_key(struct sc_input_manager *im,
switch_fps_counter_state(im);
}
return;
}
// Flatten conditions to avoid additional indentation levels
if (control) {
// Controls for all sources
switch (sdl_keycode) {
case SDLK_R:
if (!repeat && shift && down && !paused) {
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) {
reset_video(im);
}
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) {
} else {
rotate_device(im);
}
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;
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;
@@ -645,8 +562,6 @@ 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) {
@@ -718,10 +633,6 @@ 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;
@@ -757,10 +668,6 @@ 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;
@@ -809,13 +716,6 @@ 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;
@@ -890,7 +790,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_FRect *r = &im->screen->rect;
SDL_Rect *r = &im->screen->rect;
bool outside = x < r->x || x >= r->x + r->w
|| y < r->y || y >= r->y + r->h;
if (outside) {
@@ -983,10 +883,6 @@ 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;
@@ -1011,12 +907,6 @@ 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) {
@@ -1058,10 +948,6 @@ 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;
@@ -1078,10 +964,6 @@ 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;
@@ -1104,10 +986,6 @@ 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) {
@@ -1130,42 +1008,73 @@ 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:
case SDL_EVENT_DROP_FILE: {
if (!control) {
break;
}
sc_input_manager_process_file(im, &event->drop);
break;
}
}
}

View File

@@ -24,8 +24,6 @@ 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;
@@ -55,7 +53,6 @@ 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,7 +16,6 @@ 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,
@@ -114,7 +113,6 @@ 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,7 +241,6 @@ 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;
@@ -328,7 +327,6 @@ 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

@@ -444,7 +444,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
error = true;
error = false;
}
end:
@@ -541,10 +541,7 @@ 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,
const struct sc_stream_session *session) {
(void) session;
AVCodecContext *ctx) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
// only written from this thread, no need to lock
assert(!recorder->video_init);
@@ -638,10 +635,7 @@ 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,
const struct sc_stream_session *session) {
(void) session;
AVCodecContext *ctx) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock

View File

@@ -199,8 +199,8 @@ event_loop(struct scrcpy *s, bool has_screen) {
break;
}
default:
if (has_screen) {
sc_screen_handle_event(&s->screen, &event);
if (has_screen && !sc_screen_handle_event(&s->screen, &event)) {
return SCRCPY_EXIT_FAILURE;
}
break;
}
@@ -469,8 +469,6 @@ 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,
@@ -564,7 +562,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
if (options->window && options->control) {
if (options->video_playback && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
@@ -804,7 +802,6 @@ 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,
@@ -949,7 +946,7 @@ aoa_complete:
terminate_event_loop();
LOGD("quit...");
if (options->window) {
if (options->video_playback) {
// 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

@@ -144,110 +144,64 @@ sc_screen_is_relative_mode(struct sc_screen *screen) {
}
static void
compute_content_rect(struct sc_size render_size, struct sc_size content_size,
bool can_upscale, SDL_FRect *rect) {
if (is_optimal_size(render_size, content_size)) {
sc_screen_update_content_rect(struct sc_screen *screen) {
assert(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 = render_size.width;
rect->h = render_size.height;
rect->w = drawable_size.width;
rect->h = drawable_size.height;
return;
}
if (!can_upscale && content_size.width <= render_size.width
&& content_size.height <= render_size.height) {
// Center without upscaling
rect->x = (render_size.width - content_size.width) / 2.f;
rect->y = (render_size.height - content_size.height) / 2.f;
rect->w = content_size.width;
rect->h = content_size.height;
return;
}
bool keep_width = content_size.width * render_size.height
> content_size.height * render_size.width;
bool keep_width = content_size.width * drawable_size.height
> content_size.height * drawable_size.width;
if (keep_width) {
rect->x = 0;
rect->w = render_size.width;
rect->h = (float) render_size.width * content_size.height
/ content_size.width;
rect->y = (render_size.height - rect->h) / 2.f;
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 = render_size.height;
rect->w = (float) render_size.height * content_size.width
/ content_size.height;
rect->x = (render_size.width - rect->w) / 2.f;
rect->h = drawable_size.height;
rect->w = drawable_size.height * content_size.width
/ content_size.height;
rect->x = (drawable_size.width - rect->w) / 2;
}
}
static void
sc_screen_update_content_rect(struct sc_screen *screen) {
// Only upscale video frames, not icon
bool can_upscale = screen->video;
struct sc_size render_size =
sc_sdl_get_render_output_size(screen->renderer);
compute_content_rect(render_size, screen->content_size, can_upscale,
&screen->rect);
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
// changed, so that the content rectangle is recomputed
static void
sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
assert(!screen->video || screen->has_video_window);
assert(screen->video);
assert(screen->has_video_window);
if (update_content_rect) {
sc_screen_update_content_rect(screen);
}
SDL_Renderer *renderer = screen->renderer;
sc_sdl_render_clear(renderer);
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->orientation);
(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);
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
}
#if defined(__APPLE__) || defined(_WIN32)
@@ -277,11 +231,9 @@ event_watcher(void *data, SDL_Event *event) {
static bool
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx,
const struct sc_stream_session *session) {
const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
@@ -348,7 +300,6 @@ 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;
@@ -374,8 +325,7 @@ sc_screen_init(struct sc_screen *screen,
}
}
// Always create the window hidden to prevent blinking during initialization
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_HIDDEN;
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
@@ -384,7 +334,8 @@ sc_screen_init(struct sc_screen *screen,
}
if (params->video) {
// The window will be shown on first frame
window_flags |= SDL_WINDOW_RESIZABLE;
window_flags |= SDL_WINDOW_HIDDEN
| SDL_WINDOW_RESIZABLE;
}
const char *title = params->window_title;
@@ -415,46 +366,10 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
screen->renderer = SDL_CreateRenderer(screen->window, NULL);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
#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;
goto error_destroy_window;
}
SDL_Surface *icon = scrcpy_icon_load();
@@ -462,32 +377,30 @@ sc_screen_init(struct sc_screen *screen,
if (!SDL_SetWindowIcon(screen->window, icon)) {
LOGW("Could not set window icon: %s", SDL_GetError());
}
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());
}
}
scrcpy_icon_destroy(icon);
} else if (params->video) {
// just a warning
LOGW("Could not load icon");
} else {
// not fatal
// without video, the icon is used as window content, it must be present
LOGE("Could not load icon");
goto error_destroy_window;
}
if (!params->video) {
// Make sure the content size is initialized
screen->content_size.width = 256;
screen->content_size.height = 256;
}
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;
}
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_texture;
goto error_destroy_display;
}
struct sc_input_manager_params im_params = {
@@ -497,7 +410,6 @@ 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,
@@ -531,27 +443,15 @@ sc_screen_init(struct sc_screen *screen,
screen->open = false;
#endif
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);
}
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);
}
return true;
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_display:
sc_display_destroy(&screen->display);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@@ -613,12 +513,8 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_texture_destroy(&screen->tex);
sc_display_destroy(&screen->display);
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);
@@ -701,7 +597,6 @@ 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;
@@ -715,12 +610,28 @@ 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;
}
}
bool ok = sc_texture_set_from_frame(&screen->tex, frame);
if (!ok) {
enum sc_display_result res =
sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
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) {
@@ -776,10 +687,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) {
av_frame_free(&screen->frame);
screen->frame = screen->resume_frame;
screen->resume_frame = NULL;
bool ok = sc_screen_apply_frame(screen);
if (!ok) {
LOGE("Resume frame update failed");
}
sc_screen_apply_frame(screen);
}
if (!paused) {
@@ -852,7 +760,7 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
content_size.height);
}
void
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
// !video implies !has_video_window
assert(screen->video || !screen->has_video_window);
@@ -861,29 +769,32 @@ 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;
return true;
}
case SDL_EVENT_WINDOW_EXPOSED:
if (!screen->video || screen->has_video_window) {
if (!screen->video) {
sc_screen_render_novideo(screen);
} else if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return;
return true;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (screen->has_video_window) {
sc_screen_render(screen, true);
}
return;
return true;
case SDL_EVENT_WINDOW_RESTORED:
if (screen->has_video_window && is_windowed(screen)) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
return true;
case SDL_EVENT_WINDOW_ENTER_FULLSCREEN:
LOGD("Switched to fullscreen mode");
assert(screen->has_video_window);
return;
return true;
case SDL_EVENT_WINDOW_LEAVE_FULLSCREEN:
LOGD("Switched to windowed mode");
assert(screen->has_video_window);
@@ -891,16 +802,17 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
apply_pending_resize(screen);
sc_screen_render(screen, true);
}
return;
return true;
}
if (sc_screen_is_relative_mode(screen)
&& sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
return true;
}
sc_input_manager_handle_event(&screen->im, event);
return true;
}
struct sc_point

View File

@@ -12,20 +12,16 @@
#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
@@ -34,9 +30,8 @@ struct sc_screen {
#endif
bool video;
bool camera;
struct sc_texture tex;
struct sc_display display;
struct sc_input_manager im;
struct sc_mouse_capture mc; // only used in mouse relative mode
struct sc_frame_buffer fb;
@@ -53,11 +48,6 @@ 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
@@ -69,7 +59,7 @@ struct sc_screen {
// client orientation
enum sc_orientation orientation;
// rectangle of the content (excluding black borders)
struct SDL_FRect rect;
struct SDL_Rect rect;
bool has_frame;
bool has_video_window;
@@ -81,7 +71,6 @@ struct sc_screen {
struct sc_screen_params {
bool video;
bool camera;
struct sc_controller *controller;
struct sc_file_pusher *fp;
@@ -157,7 +146,8 @@ void
sc_screen_set_paused(struct sc_screen *screen, bool paused);
// react to SDL events
void
// If this function returns false, scrcpy must exit with an error.
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
// convert point from window coordinates to frame coordinates

View File

@@ -357,13 +357,6 @@ 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,7 +35,6 @@ 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;
@@ -69,7 +68,6 @@ 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;

View File

@@ -3,14 +3,26 @@
#include <processthreadsapi.h>
#include <assert.h>
#include <stdlib.h>
#include "util/command.h"
#include "util/log.h"
#include "util/str.h"
#define CMD_MAX_LEN 8192
static bool
build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
// only make it work for this very specific program
// (don't handle escaping nor quotes)
size_t ret = sc_str_join(cmd, argv, ' ', len);
if (ret >= len) {
LOGE("Command too long (%" SC_PRIsizet " chars)", len - 1);
return false;
}
return true;
}
enum sc_process_result
sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
HANDLE *pin, HANDLE *pout, HANDLE *perr) {
@@ -125,9 +137,8 @@ sc_process_execute_p(const char *const argv[], HANDLE *handle, unsigned flags,
si.lpAttributeList = lpAttributeList;
}
assert(argv && *argv);
char *cmd = sc_command_serialize_windows(argv);
if (!cmd) {
char *cmd = malloc(CMD_MAX_LEN);
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) {
LOG_OOM();
goto error_free_attribute_list;
}

View File

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

View File

@@ -1,44 +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"
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,8 +6,6 @@
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include "trait/packet_sink.h"
/**
* Frame sink trait.
*
@@ -19,16 +17,9 @@ 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,
const struct sc_stream_session *session);
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
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,12 +27,11 @@ 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 struct sc_stream_session *session) {
const AVCodecContext *ctx) {
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, session)) {
if (!sink->ops->open(sink, ctx)) {
sc_frame_source_sinks_close_firsts(source, i);
return false;
}
@@ -60,18 +59,3 @@ 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,8 +28,7 @@ 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 struct sc_stream_session *session);
const AVCodecContext *ctx);
void
sc_frame_source_sinks_close(struct sc_frame_source *source);
@@ -38,8 +37,4 @@ 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,28 +15,12 @@ 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,
const struct sc_stream_session *session);
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
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,12 +27,11 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx,
const struct sc_stream_session *session) {
AVCodecContext *ctx) {
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, session)) {
if (!sink->ops->open(sink, ctx)) {
sc_packet_source_sinks_close_firsts(source, i);
return false;
}
@@ -61,20 +60,6 @@ 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,8 +28,7 @@ sc_packet_source_add_sink(struct sc_packet_source *source,
bool
sc_packet_source_sinks_open(struct sc_packet_source *source,
AVCodecContext *ctx,
const struct sc_stream_session *session);
AVCodecContext *ctx);
void
sc_packet_source_sinks_close(struct sc_packet_source *source);
@@ -38,10 +37,6 @@ 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

@@ -9,7 +9,7 @@
# include "adb/adb.h"
#endif
#include "events.h"
#include "screen.h"
#include "usb/screen_otg.h"
#include "usb/aoa_hid.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
@@ -23,7 +23,7 @@ struct scrcpy_otg {
struct sc_mouse_aoa mouse;
struct sc_gamepad_aoa gamepad;
struct sc_screen screen;
struct sc_screen_otg screen_otg;
};
static void
@@ -49,7 +49,7 @@ event_loop(struct scrcpy_otg *s) {
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
default:
sc_screen_handle_event(&s->screen, &event);
sc_screen_otg_handle_event(&s->screen_otg, &event);
break;
}
}
@@ -88,14 +88,13 @@ scrcpy_otg(struct scrcpy_options *options) {
enum scrcpy_exit_code ret = SCRCPY_EXIT_FAILURE;
struct sc_key_processor *kp = NULL;
struct sc_mouse_processor *mp = NULL;
struct sc_gamepad_processor *gp = NULL;
struct sc_keyboard_aoa *keyboard = NULL;
struct sc_mouse_aoa *mouse = NULL;
struct sc_gamepad_aoa *gamepad = NULL;
bool usb_device_initialized = false;
bool usb_connected = false;
bool aoa_started = false;
bool aoa_initialized = false;
bool screen_initialized = false;
#ifdef _WIN32
// On Windows, only one process could open a USB device
@@ -158,7 +157,7 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
kp = &s->keyboard.key_processor;
keyboard = &s->keyboard;
}
if (enable_mouse) {
@@ -166,12 +165,12 @@ scrcpy_otg(struct scrcpy_options *options) {
if (!ok) {
goto end;
}
mp = &s->mouse.mouse_processor;
mouse = &s->mouse;
}
if (enable_gamepad) {
sc_gamepad_aoa_init(&s->gamepad, &s->aoa);
gp = &s->gamepad.gamepad_processor;
gamepad = &s->gamepad;
}
ok = sc_aoa_start(&s->aoa);
@@ -185,18 +184,10 @@ scrcpy_otg(struct scrcpy_options *options) {
window_title = usb_device.product ? usb_device.product : "scrcpy";
}
struct sc_screen_params params = {
.video = false,
.camera = false,
.controller = false,
.fp = NULL,
.kp = kp,
.mp = mp,
.gp = gp,
.mouse_bindings = options->mouse_bindings,
.legacy_paste = false,
.clipboard_autosync = false,
.shortcut_mods = options->shortcut_mods,
struct sc_screen_otg_params params = {
.keyboard = keyboard,
.mouse = mouse,
.gamepad = gamepad,
.window_title = window_title,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
@@ -204,17 +195,13 @@ scrcpy_otg(struct scrcpy_options *options) {
.window_width = options->window_width,
.window_height = options->window_height,
.window_borderless = options->window_borderless,
.orientation = SC_ORIENTATION_0,
.mipmaps = options->mipmaps,
.fullscreen = false,
.start_fps_counter = false,
.shortcut_mods = options->shortcut_mods,
};
ok = sc_screen_init(&s->screen, &params);
ok = sc_screen_otg_init(&s->screen_otg, &params);
if (!ok) {
goto end;
}
screen_initialized = true;
// usb_device not needed anymore
sc_usb_device_destroy(&usb_device);
@@ -229,17 +216,13 @@ end:
}
sc_usb_stop(&s->usb);
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
}
if (mp) {
if (mouse) {
sc_mouse_aoa_destroy(&s->mouse);
}
if (kp) {
if (keyboard) {
sc_keyboard_aoa_destroy(&s->keyboard);
}
if (gp) {
if (gamepad) {
sc_gamepad_aoa_destroy(&s->gamepad);
}
@@ -260,10 +243,5 @@ end:
sc_usb_destroy(&s->usb);
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
}
return ret;
}

327
app/src/usb/screen_otg.c Normal file
View File

@@ -0,0 +1,327 @@
#include "screen_otg.h"
#include <assert.h>
#include <stddef.h>
#include "icon.h"
#include "options.h"
#include "util/acksync.h"
#include "util/log.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);
if (!ok) {
LOGW("Could not render texture: %s", SDL_GetError());
}
}
sc_sdl_render_present(screen->renderer);
}
bool
sc_screen_otg_init(struct sc_screen_otg *screen,
const struct sc_screen_otg_params *params) {
screen->keyboard = params->keyboard;
screen->mouse = params->mouse;
screen->gamepad = params->gamepad;
const char *title = params->window_title;
assert(title);
int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED
? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED;
int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED
? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED;
int width = params->window_width ? params->window_width : 256;
int height = params->window_height ? params->window_height : 256;
uint32_t window_flags = SDL_WINDOW_HIGH_PIXEL_DENSITY;
if (params->always_on_top) {
window_flags |= SDL_WINDOW_ALWAYS_ON_TOP;
}
if (params->window_borderless) {
window_flags |= SDL_WINDOW_BORDERLESS;
}
screen->window =
sc_sdl_create_window(title, x, y, width, height, window_flags);
if (!screen->window) {
LOGE("Could not create window: %s", SDL_GetError());
return false;
}
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();
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);
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;
}
} else {
screen->texture = NULL;
LOGW("Could not load icon");
}
sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods);
if (screen->mouse) {
// Capture mouse on start
sc_mouse_capture_set_active(&screen->mc, true);
}
return true;
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
return false;
}
void
sc_screen_otg_destroy(struct sc_screen_otg *screen) {
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
}
static void
sc_screen_otg_process_key(struct sc_screen_otg *screen,
const SDL_KeyboardEvent *event) {
assert(screen->keyboard);
struct sc_key_processor *kp = &screen->keyboard->key_processor;
struct sc_key_event evt = {
.action = sc_action_from_sdl_keyboard_type(event->type),
.keycode = sc_keycode_from_sdl(event->key),
.scancode = sc_scancode_from_sdl(event->scancode),
.repeat = event->repeat,
.mods_state = sc_mods_state_from_sdl(event->mod),
};
assert(kp->ops->process_key);
kp->ops->process_key(kp, &evt, SC_SEQUENCE_INVALID);
}
static void
sc_screen_otg_process_mouse_motion(struct sc_screen_otg *screen,
const SDL_MouseMotionEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
struct sc_mouse_motion_event evt = {
// .position not used for HID events
.xrel = event->xrel,
.yrel = event->yrel,
.buttons_state = sc_mouse_buttons_state_from_sdl(event->state),
};
assert(mp->ops->process_mouse_motion);
mp->ops->process_mouse_motion(mp, &evt);
}
static void
sc_screen_otg_process_mouse_button(struct sc_screen_otg *screen,
const SDL_MouseButtonEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
struct sc_mouse_click_event evt = {
// .position not used for HID events
.action = sc_action_from_sdl_mousebutton_type(event->type),
.button = sc_mouse_button_from_sdl(event->button),
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_click);
mp->ops->process_mouse_click(mp, &evt);
}
static void
sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen,
const SDL_MouseWheelEvent *event) {
assert(screen->mouse);
struct sc_mouse_processor *mp = &screen->mouse->mouse_processor;
uint32_t sdl_buttons_state = SDL_GetMouseState(NULL, NULL);
struct sc_mouse_scroll_event evt = {
// .position not used for HID events
.hscroll = event->x,
.vscroll = event->y,
.buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state),
};
assert(mp->ops->process_mouse_scroll);
mp->ops->process_mouse_scroll(mp, &evt);
}
static void
sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen,
const SDL_GamepadDeviceEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
SDL_Gamepad *sdl_gamepad = SDL_OpenGamepad(event->which);
if (!sdl_gamepad) {
LOGW("Could not open gamepad");
return;
}
SDL_Joystick *joystick = SDL_GetGamepadJoystick(sdl_gamepad);
if (!joystick) {
LOGW("Could not get gamepad joystick");
SDL_CloseGamepad(sdl_gamepad);
return;
}
struct sc_gamepad_device_event evt = {
.gamepad_id = SDL_GetJoystickID(joystick),
};
gp->ops->process_gamepad_added(gp, &evt);
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
SDL_JoystickID id = event->which;
SDL_Gamepad *sdl_gamepad = SDL_GetGamepadFromID(id);
if (sdl_gamepad) {
SDL_CloseGamepad(sdl_gamepad);
} else {
LOGW("Unknown gamepad device removed");
}
struct sc_gamepad_device_event evt = {
.gamepad_id = id,
};
gp->ops->process_gamepad_removed(gp, &evt);
}
}
static void
sc_screen_otg_process_gamepad_axis(struct sc_screen_otg *screen,
const SDL_GamepadAxisEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_axis axis = sc_gamepad_axis_from_sdl(event->axis);
if (axis == SC_GAMEPAD_AXIS_UNKNOWN) {
return;
}
struct sc_gamepad_axis_event evt = {
.gamepad_id = event->which,
.axis = axis,
.value = event->value,
};
gp->ops->process_gamepad_axis(gp, &evt);
}
static void
sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen,
const SDL_GamepadButtonEvent *event) {
assert(screen->gamepad);
struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor;
enum sc_gamepad_button button = sc_gamepad_button_from_sdl(event->button);
if (button == SC_GAMEPAD_BUTTON_UNKNOWN) {
return;
}
struct sc_gamepad_button_event evt = {
.gamepad_id = event->which,
.action = sc_action_from_sdl_gamepad_button_type(event->type),
.button = button,
};
gp->ops->process_gamepad_button(gp, &evt);
}
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) {
if (sc_mouse_capture_handle_event(&screen->mc, event)) {
// The mouse capture handler consumed the event
return;
}
switch (event->type) {
case SDL_EVENT_WINDOW_EXPOSED:
sc_screen_otg_render(screen);
break;
case SDL_EVENT_KEY_DOWN:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_EVENT_KEY_UP:
if (screen->keyboard) {
sc_screen_otg_process_key(screen, &event->key);
}
break;
case SDL_EVENT_MOUSE_MOTION:
if (screen->mouse) {
sc_screen_otg_process_mouse_motion(screen, &event->motion);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_EVENT_MOUSE_BUTTON_UP:
if (screen->mouse) {
sc_screen_otg_process_mouse_button(screen, &event->button);
}
break;
case SDL_EVENT_MOUSE_WHEEL:
if (screen->mouse) {
sc_screen_otg_process_mouse_wheel(screen, &event->wheel);
}
break;
case SDL_EVENT_GAMEPAD_ADDED:
case SDL_EVENT_GAMEPAD_REMOVED:
// Handle device added or removed even if paused
if (screen->gamepad) {
sc_screen_otg_process_gamepad_device(screen, &event->gdevice);
}
break;
case SDL_EVENT_GAMEPAD_AXIS_MOTION:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_axis(screen, &event->gaxis);
}
break;
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
if (screen->gamepad) {
sc_screen_otg_process_gamepad_button(screen, &event->gbutton);
}
break;
}
}

52
app/src/usb/screen_otg.h Normal file
View File

@@ -0,0 +1,52 @@
#ifndef SC_SCREEN_OTG_H
#define SC_SCREEN_OTG_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <SDL3/SDL.h>
#include "mouse_capture.h"
#include "usb/gamepad_aoa.h"
#include "usb/keyboard_aoa.h"
#include "usb/mouse_aoa.h"
struct sc_screen_otg {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_mouse_capture mc;
};
struct sc_screen_otg_params {
struct sc_keyboard_aoa *keyboard;
struct sc_mouse_aoa *mouse;
struct sc_gamepad_aoa *gamepad;
const char *window_title;
bool always_on_top;
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED
uint16_t window_width;
uint16_t window_height;
bool window_borderless;
uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values
};
bool
sc_screen_otg_init(struct sc_screen_otg *screen,
const struct sc_screen_otg_params *params);
void
sc_screen_otg_destroy(struct sc_screen_otg *screen);
void
sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event);
#endif

View File

@@ -1,98 +0,0 @@
#include "command.h"
#include <stdlib.h>
#include "util/strbuf.h"
char *
sc_command_serialize_windows(const char *const argv[]) {
// <https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments>
// <https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw>
struct sc_strbuf buf;
bool ok = sc_strbuf_init(&buf, 1024);
if (!ok) {
return NULL;
}
#define BUF_PUSH(C) \
do { \
if (!sc_strbuf_append_char(&buf, C)) { \
goto end; \
} \
} while (0)
while (*argv) {
const char *arg = *argv;
BUF_PUSH('"');
// <https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments>
//
// """
// If an even number of backslashes is followed by a double quote mark,
// then one backslash (\) is placed in the argv array for every pair of
// backslashes (\\), and the double quote mark (") is interpreted as a
// string delimiter.
//
// If an odd number of backslashes is followed by a double quote mark,
// then one backslash (\) is placed in the argv array for every pair of
// backslashes (\\). The double quote mark is interpreted as an escape
// sequence by the remaining backslash, causing a literal double quote
// mark (") to be placed in argv.
// """
//
// To produce correct escaping according to what the parser will do, we
// must count the number of successive backslashes.
unsigned backslashes = 0;
for (const char *c = arg; *c; c++) {
switch (*c) {
case '"':
while (backslashes) {
// Double all backslashes before a quote
BUF_PUSH('\\');
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH('\\');
BUF_PUSH('"');
backslashes = 0;
break;
case '\\':
++backslashes;
break;
default:
while (backslashes) {
// Put all backslashes as literals
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH(*c);
break;
}
}
while (backslashes) {
// Double all backslashes before a quote
BUF_PUSH('\\');
BUF_PUSH('\\');
--backslashes;
}
BUF_PUSH('"');
++argv;
// Argument separator
if (*argv) {
BUF_PUSH(' ');
}
}
return buf.s;
end:
free(buf.s);
return NULL;
}

View File

@@ -1,17 +0,0 @@
#ifndef SC_COMMAND_H
#define SC_COMMAND_H
#include "common.h"
/**
* Serialize an argv array for Windows
*
* Convert a NULL-terminated argument array into a single escaped string
* suitable for massing to CreateProcess() on Windows.
*
* The returned value must be freed by the caller.
*/
char *
sc_command_serialize_windows(const char *const argv[]);
#endif

View File

@@ -151,10 +151,6 @@ sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
void
sc_log_configure(void) {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
SDL_SetLogOutputFunction(sc_sdl_log_print, NULL);
// Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback);

View File

@@ -130,25 +130,6 @@ 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,9 +34,6 @@ 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

@@ -49,6 +49,21 @@ truncated:
return n;
}
char *
sc_str_quote(const char *src) {
size_t len = strlen(src);
char *quoted = malloc(len + 3);
if (!quoted) {
LOG_OOM();
return NULL;
}
memcpy(&quoted[1], src, len);
quoted[0] = '"';
quoted[len + 1] = '"';
quoted[len + 2] = '\0';
return quoted;
}
char *
sc_str_concat(const char *start, const char *end) {
assert(start);

View File

@@ -32,6 +32,14 @@ sc_strncpy(char *dest, const char *src, size_t n);
size_t
sc_str_join(char *dst, const char *const tokens[], char sep, size_t n);
/**
* Quote a string
*
* Return a new allocated string, surrounded with quotes (`"`).
*/
char *
sc_str_quote(const char *src);
/**
* Concat two strings
*

View File

@@ -146,11 +146,9 @@ run_v4l2_sink(void *data) {
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx,
const struct sc_stream_session *session) {
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
(void) session;
bool ok = sc_frame_buffer_init(&vs->fb);
if (!ok) {
@@ -328,10 +326,9 @@ 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,
const struct sc_stream_session *session) {
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs, ctx, session);
return sc_v4l2_sink_open(vs, ctx);
}
static void

View File

@@ -1,61 +0,0 @@
#include "common.h"
#include <assert.h>
#include <stdlib.h>
#include "util/command.h"
static void test_command_with_spaces(void) {
const char *const argv[] = {
"C:\\Program Files\\scrcpy\\adb",
"-s",
"serial with spaces",
"push",
"E:\\some folder\\scrcpy-server",
"/data/local/tmp/scrcpy-server.jar",
NULL,
};
char *cmd = sc_command_serialize_windows(argv);
const char *expected = "\"C:\\Program Files\\scrcpy\\adb\" "
"\"-s\" "
"\"serial with spaces\" "
"\"push\" "
"\"E:\\some folder\\scrcpy-server\" "
"\"/data/local/tmp/scrcpy-server.jar\"";
assert(!strcmp(expected, cmd));
free(cmd);
}
static void test_command_with_backslashes(void) {
const char *const argv[] = {
"a\\\\ b\\",
"def \\",
"gh\"i\" \\\\",
"jkl\\\\",
"mno\\",
"p\\\"qr",
NULL,
};
char *cmd = sc_command_serialize_windows(argv);
const char *expected = "\"a\\\\ b\\\\\" "
"\"def \\\\\" "
"\"gh\\\"i\\\" \\\\\\\\\" "
"\"jkl\\\\\\\\\" "
"\"mno\\\\\" "
"\"p\\\\\\\"qr\"";
assert(!strcmp(expected, cmd));
free(cmd);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_command_with_spaces();
test_command_with_backslashes();
return 0;
}

View File

@@ -446,55 +446,6 @@ 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;
@@ -519,8 +470,5 @@ 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

@@ -131,6 +131,16 @@ static void test_join_truncated_after_sep(void) {
assert(!strcmp("abc de ", s));
}
static void test_quote(void) {
const char *s = "abcde";
char *out = sc_str_quote(s);
// add '"' at the beginning and the end
assert(!strcmp("\"abcde\"", out));
free(out);
}
static void test_concat(void) {
const char *s = "2024:11";
char *out = sc_str_concat("my-prefix:", s);
@@ -388,6 +398,7 @@ int main(int argc, char *argv[]) {
test_join_truncated_in_token();
test_join_truncated_before_sep();
test_join_truncated_after_sep();
test_quote();
test_concat();
test_utf8_truncate();
test_parse_integer();

73
container/Dockerfile Normal file
View File

@@ -0,0 +1,73 @@
# Build with:
# podman build -t scrcpy-builder .
#
# Run with:
# podman run \
# -v PATH_TO_SCRCPY:/home/debian/scrcpy \
# --userns=keep-id \
# -it scrcpy-builder \
# bash
FROM debian:trixie
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
sudo wget unzip gcc git pkg-config meson ninja-build cmake \
libsdl3-dev libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0-dev openjdk-21-jdk \
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
RUN groupadd -g 1000 debian \
&& useradd -m -u 1000 -g 1000 -s /bin/bash debian
RUN echo "debian ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
ENV ANDROID_HOME=/opt/android/sdk
RUN mkdir -p "$ANDROID_HOME" \
&& chown debian:debian "$ANDROID_HOME"
USER debian
WORKDIR /home/debian
ENV CMDLINETOOLS_URL=https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
ENV CMDLINETOOLS_SHA256=7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee
RUN wget -q "$CMDLINETOOLS_URL" -O cmdlinetools.zip \
&& echo "$CMDLINETOOLS_SHA256 cmdlinetools.zip" | sha256sum -c
RUN mkdir tmp \
&& unzip -q cmdlinetools.zip -d tmp \
&& mkdir -p "$ANDROID_HOME/cmdline-tools" \
&& mv tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest" \
&& rmdir tmp \
&& rm cmdlinetools.zip
# To get the licence hash, run manually: "$ANDROID_HOME"/cmdline-tools/latest/bin/sdkmanager --licenses
# Build-tools 35 is still needed for the current AGP version
RUN mkdir -p "$ANDROID_HOME/licenses" \
&& echo 24333f8a63b6825ea9c5514f83c2829b004d1fee > "$ANDROID_HOME/licenses/android-sdk-license" \
&& $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-36" "build-tools;35.0.0" "build-tools;36.0.0"
# For scrcpy build scripts
ENV GRADLE_VERSION=8.14.3
ENV GRADLE_HOME=/opt/gradle-$GRADLE_VERSION
ENV GRADLE_URL=https://services.gradle.org/distributions/gradle-$GRADLE_VERSION-bin.zip
ENV GRADLE_SHA256=bd71102213493060956ec229d946beee57158dbd89d0e62b91bca0fa2c5f3531
ENV GRADLE=gradle
ENV PATH=$GRADLE_HOME/bin:$PATH
USER root
RUN wget -q "$GRADLE_URL" -O gradle.zip \
&& echo "$GRADLE_SHA256 gradle.zip" | sha256sum -c
RUN unzip -q gradle.zip -d /opt \
&& rm gradle.zip
USER debian
# Pre-download gradle dependencies for scrcpy
RUN mkdir -p /home/debian/fake-scrcpy/app
COPY fake.gradle /home/debian/fake-scrcpy/build.gradle
COPY fake_app.gradle /home/debian/fake-scrcpy/app/build.gradle
RUN echo "include ':app'" > /home/debian/fake-scrcpy/settings.gradle \
&& gradle -p /home/debian/fake-scrcpy dependencies androidDependencies --no-daemon \
&& rm -rf /home/debian/fake-scrcpy

17
container/fake.gradle Normal file
View File

@@ -0,0 +1,17 @@
// Fake build.gradle to pre-download gradle dependencies
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}

21
container/fake_app.gradle Normal file
View File

@@ -0,0 +1,21 @@
// Fake app/build.gradle to pre-download gradle dependencies
apply plugin: 'com.android.application'
apply plugin: 'checkstyle'
android {
buildToolsVersion = "36.0.0"
namespace = "com.genymobile.scrcpy"
compileSdk 36
defaultConfig {
minSdkVersion 21
targetSdkVersion 36
}
}
checkstyle {
toolVersion = '10.12.5'
}
dependencies {
testImplementation 'junit:junit:4.13.2'
}

View File

@@ -165,30 +165,6 @@ 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,11 +409,12 @@ 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 device metadata (in practice, the device
- `send_device_meta=false`: disable the device metata (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_stream_meta`: disable codec and video size metadata
- `send_codec_meta`: disable the codec information (and initial device size for
video)
- `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,10 +58,6 @@ _<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

@@ -44,10 +44,8 @@ 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;
@@ -80,7 +78,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 sendStreamMeta = true; // write the stream metadata (codec and session)
private boolean sendCodecMeta = true; // write the codec metadata before the stream
public Ln.Level getLogLevel() {
return logLevel;
@@ -170,10 +168,6 @@ public class Options {
return cameraAspectRatio;
}
public float getCameraZoom() {
return cameraZoom;
}
public int getCameraFps() {
return cameraFps;
}
@@ -182,10 +176,6 @@ public class Options {
return cameraHighSpeed;
}
public boolean getCameraTorch() {
return cameraTorch;
}
public boolean getShowTouches() {
return showTouches;
}
@@ -294,8 +284,8 @@ public class Options {
return sendDummyByte;
}
public boolean getSendStreamMeta() {
return sendStreamMeta;
public boolean getSendCodecMeta() {
return sendCodecMeta;
}
@SuppressWarnings("MethodLength")
@@ -479,20 +469,12 @@ 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;
@@ -519,8 +501,8 @@ public class Options {
case "send_dummy_byte":
options.sendDummyByte = Boolean.parseBoolean(value);
break;
case "send_stream_meta":
options.sendStreamMeta = Boolean.parseBoolean(value);
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_stream":
boolean rawStream = Boolean.parseBoolean(value);
@@ -528,7 +510,7 @@ public class Options {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendStreamMeta = false;
options.sendCodecMeta = 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.getSendStreamMeta(), options.getSendFrameMeta());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), 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.getSendStreamMeta(),
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta());
SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {

View File

@@ -25,9 +25,6 @@ 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;
@@ -169,13 +166,6 @@ 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,8 +47,6 @@ 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();
@@ -58,8 +56,6 @@ 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);
}
@@ -170,11 +166,6 @@ 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,9 +12,7 @@ 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;
@@ -77,7 +75,6 @@ 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;
@@ -100,26 +97,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean keepDisplayPowerOff;
// Used for resetting video encoding on RESET_VIDEO message or for sending camera controls
// Used for resetting video encoding on RESET_VIDEO message
private SurfaceCapture surfaceCapture;
public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) {
this.camera = options.getVideoSource() == VideoSource.CAMERA;
this.displayId = options.getDisplayId();
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();
@@ -217,7 +201,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void control() throws IOException {
// on start, power on the device
if (!camera && powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) {
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
// dirty hack
@@ -252,9 +236,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
}
}, "control-recv");
thread.start();
if (sender != null) {
sender.start();
}
sender.start();
}
@Override
@@ -262,9 +244,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (thread != null) {
thread.interrupt();
}
if (sender != null) {
sender.stop();
}
sender.stop();
}
@Override
@@ -272,122 +252,90 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
if (thread != null) {
thread.join();
}
if (sender != null) {
sender.join();
}
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;
}
int type = msg.getType();
// Events for all sources (display or camera)
switch (type) {
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;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
return true;
break;
default:
// fall through
// do nothing
}
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);
return true;
}
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
@@ -803,7 +751,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private void resetVideo() {
if (surfaceCapture != null) {
Ln.i("Video capture reset");
surfaceCapture.invalidate();
surfaceCapture.requestInvalidate();
}
}
}

View File

@@ -14,13 +14,12 @@ import java.util.Arrays;
public final class Streamer {
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 static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private final FileDescriptor fd;
private final Codec codec;
private final boolean sendStreamMeta;
private final boolean sendCodecMeta;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
@@ -28,7 +27,7 @@ public final class Streamer {
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
this.fd = fd;
this.codec = codec;
this.sendStreamMeta = sendCodecMeta;
this.sendCodecMeta = sendCodecMeta;
this.sendFrameMeta = sendFrameMeta;
}
@@ -37,7 +36,7 @@ public final class Streamer {
}
public void writeAudioHeader() throws IOException {
if (sendStreamMeta) {
if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
buffer.flip();
@@ -45,10 +44,12 @@ public final class Streamer {
}
}
public void writeVideoHeader() throws IOException {
if (sendStreamMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4);
public void writeVideoHeader(Size videoSize) throws IOException {
if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(12);
buffer.putInt(codec.getId());
buffer.putInt(videoSize.getWidth());
buffer.putInt(videoSize.getHeight());
buffer.flip();
IO.writeFully(fd, buffer);
}
@@ -88,18 +89,6 @@ 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,7 +23,6 @@ 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;
@@ -165,7 +164,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) {
String uniqueLowFps = getFormattedUniqueSet(lowFpsRanges);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append(", fps=").append(uniqueLowFps);
}
} catch (Exception e) {
@@ -173,18 +172,6 @@ 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) {
@@ -204,7 +191,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();
String uniqueHighFps = getFormattedUniqueSet(highFpsRanges);
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
}
@@ -218,31 +205,14 @@ public final class LogUtils {
return builder.toString();
}
private static String getFormattedUniqueSet(Range<Integer>[] ranges) {
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) {
set.add(range.getUpper());
}
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();
return set;
}
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,7 +36,6 @@ 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;
@@ -54,8 +53,6 @@ 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;
@@ -66,13 +63,10 @@ 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;
@@ -84,11 +78,6 @@ 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();
@@ -101,8 +90,6 @@ 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
@@ -263,7 +250,6 @@ 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) {
@@ -275,68 +261,11 @@ 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 {
cameraDevice.createCaptureSession(sessionConfig);
} catch (CameraAccessException e) {
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest request = createCaptureRequest(surface);
setRepeatingRequest(session, request);
} catch (CameraAccessException | InterruptedException e) {
stop();
throw new IOException(e);
}
@@ -344,13 +273,6 @@ 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;
@@ -431,7 +353,46 @@ public class CameraCapture extends SurfaceCapture {
}
@TargetApi(AndroidVersions.API_31_ANDROID_12)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException {
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 {
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
@@ -458,63 +419,8 @@ public class CameraCapture extends SurfaceCapture {
return disconnected.get();
}
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;
@Override
public void requestInvalidate() {
// do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring)
}
}

View File

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

View File

@@ -211,4 +211,9 @@ 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, or due to a manual user request).
* Notify the listener that the capture has been invalidated (for example, because its size changed).
*/
public void invalidate() {
protected void invalidate() {
listener.onInvalidated();
}
@@ -86,4 +86,11 @@ 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,13 +71,16 @@ public class SurfaceEncoder implements AsyncProcessor {
try {
boolean alive;
streamer.writeVideoHeader();
boolean headerWritten = false;
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());
@@ -104,7 +107,6 @@ 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,56 +422,6 @@ 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();