Compare commits

..

8 Commits

Author SHA1 Message Date
f996386b6e Replace try-with-resources
LocalServerSocket was not AutoCloseable in older Android SDKs.
2023-03-31 00:24:01 +02:00
cfc9882897 Adapt FakeContext to API 23 2023-03-31 00:24:01 +02:00
e4c152b1a3 Call Builder.setContext() by reflection 2023-03-31 00:24:01 +02:00
6c5b20fdb1 Call AudioRecord.getTimestamp() by reflection 2023-03-31 00:24:01 +02:00
512ef4e5c0 Use literals for missing KeyCodes 2023-03-31 00:24:01 +02:00
186a5fdcff Use literal for MIMETYPE_VIDEO_AV1 2023-03-31 00:24:01 +02:00
fb3d09b7e3 Use literals for Build.VERSION_CODES.* 2023-03-31 00:24:01 +02:00
ce3d7507ce Add AttributionSource stub
The class was not present in older Android SDKs.
2023-03-31 00:24:01 +02:00
53 changed files with 821 additions and 1516 deletions

View File

@ -29,17 +29,14 @@ _scrcpy() {
-M --hid-mouse
-m --max-size=
--no-audio
--no-audio-playback
--no-cleanup
--no-clipboard-autosync
--no-downsize-on-error
-n --no-control
-N --no-playback
-N --no-display
--no-key-repeat
--no-mipmaps
--no-power-on
--no-video
--no-video-playback
--otg
-p --port=
--power-off-on-close

View File

@ -35,17 +35,14 @@ arguments=(
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]'
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
{-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--power-off-on-close[Turn the device screen off when closing scrcpy]'

View File

@ -14,7 +14,6 @@ src = [
'src/delay_buffer.c',
'src/demuxer.c',
'src/device_msg.c',
'src/display.c',
'src/icon.c',
'src/file_pusher.c',
'src/fps_counter.c',

View File

@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.0-scrcpy-4
VERSION=6.0-scrcpy-2
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14
if [[ -d "$DEP_DIR" ]]
then

View File

@ -183,14 +183,6 @@ It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR.
.TP
.B \-\-no\-audio
Disable audio forwarding.
.TP
.B \-\-no\-audio\-playback
Disable audio playback on the computer.
.TP
.B \-\-no\-cleanup
By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.
@ -214,8 +206,8 @@ This option disables this behavior.
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback).
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP
.B \-\-no\-key\-repeat
@ -229,14 +221,6 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically
.B \-\-no\-power\-on
Do not power on the device on start.
.TP
.B \-\-no\-video
Disable video forwarding.
.TP
.B \-\-no\-video\-playback
Disable video playback on the computer.
.TP
.B \-\-otg
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.

View File

@ -204,7 +204,6 @@ sc_adb_parse_device_ip(char *str) {
while (str[idx_line] != '\0') {
char *line = &str[idx_line];
size_t len = strcspn(line, "\n");
bool is_last_line = line[len] == '\0';
// The same, but without any trailing '\r'
size_t line_len = sc_str_remove_trailing_cr(line, len);
@ -216,12 +215,12 @@ sc_adb_parse_device_ip(char *str) {
return ip;
}
if (is_last_line) {
break;
}
idx_line += len;
// The next line starts after the '\n'
idx_line += len + 1;
if (str[idx_line] != '\0') {
// The next line starts after the '\n'
++idx_line;
}
}
return NULL;

View File

@ -72,10 +72,6 @@ enum {
OPT_REQUIRE_AUDIO,
OPT_AUDIO_BUFFER,
OPT_AUDIO_OUTPUT_BUFFER,
OPT_NO_DISPLAY,
OPT_NO_VIDEO,
OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK,
};
struct sc_option {
@ -353,11 +349,6 @@ static const struct sc_option options[] = {
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{
.longopt_id = OPT_NO_AUDIO_PLAYBACK,
.longopt = "no-audio-playback",
.text = "Disable audio playback on the computer.",
},
{
.longopt_id = OPT_NO_CLEANUP,
.longopt = "no-cleanup",
@ -389,14 +380,9 @@ static const struct sc_option options[] = {
},
{
.shortopt = 'N',
.longopt = "no-playback",
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
// deprecated
.longopt_id = OPT_NO_DISPLAY,
.longopt = "no-display",
.text = "Do not display device (only when screen recording or V4L2 "
"sink is enabled).",
},
{
.longopt_id = OPT_NO_KEY_REPEAT,
@ -415,16 +401,6 @@ static const struct sc_option options[] = {
.longopt = "no-power-on",
.text = "Do not power on the device on start.",
},
{
.longopt_id = OPT_NO_VIDEO,
.longopt = "no-video",
.text = "Disable video forwarding.",
},
{
.longopt_id = OPT_NO_VIDEO_PLAYBACK,
.longopt = "no-video-playback",
.text = "Disable video playback on the computer.",
},
{
.longopt_id = OPT_OTG,
.longopt = "otg",
@ -1491,39 +1467,18 @@ sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
}
#endif
static enum sc_record_format
get_record_format(const char *name) {
if (!strcmp(name, "mp4")) {
return SC_RECORD_FORMAT_MP4;
}
if (!strcmp(name, "mkv")) {
return SC_RECORD_FORMAT_MKV;
}
if (!strcmp(name, "m4a")) {
return SC_RECORD_FORMAT_M4A;
}
if (!strcmp(name, "mka")) {
return SC_RECORD_FORMAT_MKA;
}
if (!strcmp(name, "opus")) {
return SC_RECORD_FORMAT_OPUS;
}
if (!strcmp(name, "aac")) {
return SC_RECORD_FORMAT_AAC;
}
return 0;
}
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
enum sc_record_format fmt = get_record_format(optarg);
if (!fmt) {
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
if (!strcmp(optarg, "mp4")) {
*format = SC_RECORD_FORMAT_MP4;
return true;
}
*format = fmt;
return true;
if (!strcmp(optarg, "mkv")) {
*format = SC_RECORD_FORMAT_MKV;
return true;
}
LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg);
return false;
}
static bool
@ -1543,13 +1498,18 @@ parse_port(const char *optarg, uint16_t *port) {
static enum sc_record_format
guess_record_format(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot) {
size_t len = strlen(filename);
if (len < 4) {
return 0;
}
const char *ext = dot + 1;
return get_record_format(ext);
const char *ext = &filename[len - 4];
if (!strcmp(ext, ".mp4")) {
return SC_RECORD_FORMAT_MP4;
}
if (!strcmp(ext, ".mkv")) {
return SC_RECORD_FORMAT_MKV;
}
return 0;
}
static bool
@ -1682,18 +1642,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case 'n':
opts->control = false;
break;
case OPT_NO_DISPLAY:
LOGW("--no-display is deprecated, use --no-playback instead.");
// fall through
case 'N':
opts->video_playback = false;
opts->audio_playback = false;
break;
case OPT_NO_VIDEO_PLAYBACK:
opts->video_playback = false;
break;
case OPT_NO_AUDIO_PLAYBACK:
opts->audio_playback = false;
opts->display = false;
break;
case 'p':
if (!parse_port_range(optarg, &opts->port_range)) {
@ -1838,9 +1788,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_DOWNSIZE_ON_ERROR:
opts->downsize_on_error = false;
break;
case OPT_NO_VIDEO:
opts->video = false;
break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
@ -1891,8 +1838,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
#else
LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this "
"platform).");
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
return false;
#endif
case OPT_LIST_ENCODERS:
@ -1943,52 +1889,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
bool otg = false;
bool v4l2 = false;
#ifdef HAVE_USB
otg = opts->otg;
#endif
#ifdef HAVE_V4L2
v4l2 = !!opts->v4l2_device;
#endif
if (!opts->video) {
opts->video_playback = false;
}
if (!opts->audio) {
opts->audio_playback = false;
}
if (!opts->video_playback && !otg) {
// If video playback is disabled and OTG are disabled, then there is
// no way to control the device.
opts->control = false;
}
if (opts->video && !opts->video_playback && !opts->record_filename
&& !v4l2) {
LOGI("No video playback, no recording, no V4L2 sink: video disabled");
opts->video = false;
}
if (opts->audio && !opts->audio_playback && !opts->record_filename) {
LOGI("No audio playback, no recording: audio disabled");
opts->audio = false;
}
if (!opts->video && !opts->audio && !otg) {
LOGE("No video, no audio, no OTG: nothing to do");
if (!opts->display && !opts->record_filename && !opts->v4l2_device) {
LOGE("-N/--no-display requires either screen recording (-r/--record)"
" or sink to v4l2loopback device (--v4l2-sink)");
return false;
}
if (!opts->video && !otg) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
#ifdef HAVE_V4L2
if (v4l2) {
if (opts->v4l2_device) {
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
@ -2006,8 +1914,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("V4L2 buffer value without V4L2 sink\n");
return false;
}
#else
if (!opts->display && !opts->record_filename) {
LOGE("-N/--no-display requires screen recording (-r/--record)");
return false;
}
#endif
if (opts->audio && !opts->display && !opts->record_filename) {
LOGI("No display and no recording: audio disabled");
opts->audio = false;
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
@ -2019,41 +1937,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
if (opts->record_filename) {
if (opts->record_filename && !opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
LOGE("No format specified for \"%s\" "
"(try with --record-format=mkv)",
opts->record_filename);
return false;
}
}
if (opts->audio_codec == SC_CODEC_RAW) {
LOGW("Recording does not support RAW audio codec");
LOGE("No format specified for \"%s\" "
"(try with --record-format=mkv)",
opts->record_filename);
return false;
}
}
if (opts->video
&& sc_record_format_is_audio_only(opts->record_format)) {
LOGE("Audio container does not support video stream");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_OPUS
&& opts->audio_codec != SC_CODEC_OPUS) {
LOGE("Recording to OPUS file requires an OPUS audio stream "
"(try with --audio-codec=opus)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_AAC
&& opts->audio_codec != SC_CODEC_AAC) {
LOGE("Recording to AAC file requires an AAC audio stream "
"(try with --audio-codec=aac)");
return false;
}
if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) {
LOGW("Recording does not support RAW audio codec");
return false;
}
if (opts->audio_codec == SC_CODEC_RAW) {
@ -2087,9 +1983,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
#ifdef HAVE_USB
# ifdef _WIN32
if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) {
if (!opts->otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID)) {
LOGE("On Windows, it is not possible to open a USB device already open "
"by another process (like adb).");
LOGE("Therefore, -K/--hid-keyboard and -M/--hid-mouse may only work in "
@ -2098,7 +1996,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
# endif
if (otg) {
if (opts->otg) {
// OTG mode is compatible with only very few options.
// Only report obvious errors.
if (opts->record_filename) {
@ -2125,11 +2023,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("OTG mode: could not select display");
return false;
}
if (v4l2) {
# ifdef HAVE_V4L2
if (opts->v4l2_device) {
LOGE("OTG mode: could not sink to V4L2 device");
return false;
}
# endif
}
#endif
return true;
}

View File

@ -25,12 +25,6 @@
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif
// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added
// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag
// n3.3).
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100)
# define SCRCPY_LAVC_HAS_AV1
#endif
// In ffmpeg/doc/APIchanges:
// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h

View File

@ -33,12 +33,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1:
#ifdef SCRCPY_LAVC_HAS_AV1
return AV_CODEC_ID_AV1;
#else
LOGE("AV1 not supported by this FFmpeg version");
return AV_CODEC_ID_NONE;
#endif
case SC_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC:

View File

@ -1,285 +0,0 @@
#include "display.h"
#include <assert.h>
#include "util/log.h"
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
display->renderer =
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (!display->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
return false;
}
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(display->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
display->mipmaps = false;
// 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.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
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->pending.flags = 0;
display->pending.frame = NULL;
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_DeleteContext(display->gl_context);
#endif
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
SDL_DestroyRenderer(display->renderer);
}
static SDL_Texture *
sc_display_create_texture(struct sc_display *display,
struct sc_size size) {
SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
LOGD("Could not create texture: %s", SDL_GetError());
return NULL;
}
if (display->mipmaps) {
struct sc_opengl *gl = &display->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
// 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);
SDL_GL_UnbindTexture(texture);
}
return texture;
}
static inline void
sc_display_set_pending_size(struct sc_display *display, struct sc_size size) {
assert(!display->texture);
display->pending.size = size;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE;
}
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;
}
}
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;
}
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display, display->pending.size);
if (!display->texture) {
return false;
}
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE;
}
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
bool ok = sc_display_update_texture(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_set_texture_size_internal(struct sc_display *display,
struct sc_size size) {
assert(size.width && size.height);
if (display->texture) {
SDL_DestroyTexture(display->texture);
}
display->texture = sc_display_create_texture(display, size);
if (!display->texture) {
return false;
}
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
return true;
}
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
bool ok = sc_display_set_texture_size_internal(display, size);
if (!ok) {
sc_display_set_pending_size(display, size);
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
if (ret) {
LOGD("Could not update texture: %s", SDL_GetError());
return false;
}
if (display->mipmaps) {
SDL_GL_BindTexture(display->texture, NULL, NULL);
display->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(display->texture);
}
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,
unsigned rotation) {
SDL_RenderClear(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 (rotation == 0) {
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - rotation) % 4;
double angle = 90 * cw_rotation;
const SDL_Rect *dstrect = NULL;
SDL_Rect rect;
if (rotation & 1) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
rect.w = geometry->h;
rect.h = geometry->w;
dstrect = &rect;
} else {
assert(rotation == 2);
dstrect = geometry;
}
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
NULL, 0);
if (ret) {
LOGE("Could not render texture: %s", SDL_GetError());
return SC_DISPLAY_RESULT_ERROR;
}
}
SDL_RenderPresent(display->renderer);
return SC_DISPLAY_RESULT_OK;
}

View File

@ -1,59 +0,0 @@
#ifndef SC_DISPLAY_H
#define SC_DISPLAY_H
#include "common.h"
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL.h>
#include "coords.h"
#include "opengl.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;
struct {
#define SC_DISPLAY_PENDING_FLAG_SIZE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct sc_size size;
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, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
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,
unsigned rotation);
#endif

View File

@ -797,8 +797,7 @@ 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) {
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
bool control = im->controller;
switch (event->type) {
case SDL_TEXTINPUT:

View File

@ -61,7 +61,6 @@ sc_input_manager_init(struct sc_input_manager *im,
const struct sc_input_manager_params *params);
void
sc_input_manager_handle_event(struct sc_input_manager *im,
const SDL_Event *event);
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event);
#endif

View File

@ -11,6 +11,9 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_codec_options = NULL,
.video_encoder = NULL,
.audio_encoder = NULL,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
#endif
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
@ -39,12 +42,9 @@ const struct scrcpy_options scrcpy_options_default = {
.window_height = 0,
.display_id = 0,
.display_buffer = 0,
.v4l2_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,
#endif
#ifdef HAVE_USB
.otg = false,
#endif
@ -52,8 +52,7 @@ const struct scrcpy_options scrcpy_options_default = {
.fullscreen = false,
.always_on_top = false,
.control = true,
.video_playback = true,
.audio_playback = true,
.display = true,
.turn_screen_off = false,
.key_inject_mode = SC_KEY_INJECT_MODE_MIXED,
.window_borderless = false,
@ -74,7 +73,6 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.video = true,
.audio = true,
.require_audio = false,
.list_encoders = false,

View File

@ -21,20 +21,8 @@ enum sc_record_format {
SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV,
SC_RECORD_FORMAT_M4A,
SC_RECORD_FORMAT_MKA,
SC_RECORD_FORMAT_OPUS,
SC_RECORD_FORMAT_AAC,
};
static inline bool
sc_record_format_is_audio_only(enum sc_record_format fmt) {
return fmt == SC_RECORD_FORMAT_M4A
|| fmt == SC_RECORD_FORMAT_MKA
|| fmt == SC_RECORD_FORMAT_OPUS
|| fmt == SC_RECORD_FORMAT_AAC;
}
enum sc_codec {
SC_CODEC_H264,
SC_CODEC_H265,
@ -112,6 +100,9 @@ struct scrcpy_options {
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
#ifdef HAVE_V4L2
const char *v4l2_device;
#endif
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
@ -134,12 +125,9 @@ struct scrcpy_options {
uint16_t window_height;
uint32_t display_id;
sc_tick display_buffer;
sc_tick v4l2_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;
#endif
#ifdef HAVE_USB
bool otg;
#endif
@ -147,8 +135,7 @@ struct scrcpy_options {
bool fullscreen;
bool always_on_top;
bool control;
bool video_playback;
bool audio_playback;
bool display;
bool turn_screen_off;
enum sc_key_inject_mode key_inject_mode;
bool window_borderless;
@ -169,7 +156,6 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool video;
bool audio;
bool require_audio;
bool list_encoders;

View File

@ -60,17 +60,9 @@ sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
static const char *
sc_recorder_get_format_name(enum sc_record_format format) {
switch (format) {
case SC_RECORD_FORMAT_MP4:
case SC_RECORD_FORMAT_M4A:
case SC_RECORD_FORMAT_AAC:
return "mp4";
case SC_RECORD_FORMAT_MKV:
case SC_RECORD_FORMAT_MKA:
return "matroska";
case SC_RECORD_FORMAT_OPUS:
return "opus";
default:
return NULL;
case SC_RECORD_FORMAT_MP4: return "mp4";
case SC_RECORD_FORMAT_MKV: return "matroska";
default: return NULL;
}
}
@ -160,7 +152,7 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
static inline bool
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
@ -184,7 +176,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
}
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->stopped);
// If the recorder is stopped, don't process anything if there are not
// at least video packets
@ -192,11 +184,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
return false;
}
AVPacket *video_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->video);
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
}
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue);
AVPacket *audio_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
@ -208,19 +196,17 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
int ret = false;
if (video_pkt) {
if (video_pkt->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
}
if (video_pkt->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
}
assert(recorder->video_stream_index >= 0);
AVStream *video_stream =
recorder->ctx->streams[recorder->video_stream_index];
bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
if (!ok) {
goto end;
}
assert(recorder->video_stream_index >= 0);
AVStream *video_stream =
recorder->ctx->streams[recorder->video_stream_index];
bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
if (!ok) {
goto end;
}
if (audio_pkt) {
@ -232,13 +218,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
assert(recorder->audio_stream_index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
if (!ok) {
goto end;
}
}
bool ok = avformat_write_header(recorder->ctx, NULL) >= 0;
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) {
LOGE("Failed to write header to %s", recorder->filename);
goto end;
@ -247,9 +233,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
ret = true;
end:
if (video_pkt) {
av_packet_free(&video_pkt);
}
av_packet_free(&video_pkt);
if (audio_pkt) {
av_packet_free(&audio_pkt);
}
@ -279,8 +263,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) {
if (recorder->video && !video_pkt &&
!sc_vecdeque_is_empty(&recorder->video_queue)) {
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed
break;
}
@ -295,11 +278,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
// If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping.
// If there is no video, then the video_queue will remain empty forever
// and video_pkt will always be NULL.
assert(recorder->video || (!video_pkt
&& sc_vecdeque_is_empty(&recorder->video_queue)));
// If there is no audio, then the audio_queue will remain empty forever
// and audio_pkt will always be NULL.
assert(recorder->audio || (!audio_pkt
@ -341,9 +319,6 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
if (!recorder->audio) {
assert(video_pkt);
pts_origin = video_pkt->pts;
} else if (!recorder->video) {
assert(audio_pkt);
pts_origin = audio_pkt->pts;
} else if (video_pkt && audio_pkt) {
pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
} else if (recorder->stopped) {
@ -664,7 +639,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio,
enum sc_record_format format, bool audio,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
@ -687,8 +662,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
goto error_queue_cond_destroy;
}
assert(video || audio);
recorder->video = video;
recorder->audio = audio;
sc_vecdeque_init(&recorder->video_queue);
@ -707,15 +680,13 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata;
if (video) {
static const struct sc_packet_sink_ops video_ops = {
.open = sc_recorder_video_packet_sink_open,
.close = sc_recorder_video_packet_sink_close,
.push = sc_recorder_video_packet_sink_push,
};
static const struct sc_packet_sink_ops video_ops = {
.open = sc_recorder_video_packet_sink_open,
.close = sc_recorder_video_packet_sink_close,
.push = sc_recorder_video_packet_sink_push,
};
recorder->video_packet_sink.ops = &video_ops;
}
recorder->video_packet_sink.ops = &video_ops;
if (audio) {
static const struct sc_packet_sink_ops audio_ops = {

View File

@ -27,7 +27,6 @@ struct sc_recorder {
* may access it without data races.
*/
bool audio;
bool video;
char *filename;
enum sc_record_format format;
@ -60,7 +59,7 @@ struct sc_recorder_callbacks {
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio,
enum sc_record_format format, bool audio,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool

View File

@ -137,7 +137,7 @@ sdl_set_hints(const char *render_driver) {
}
static void
sdl_configure(bool video_playback, bool disable_screensaver) {
sdl_configure(bool display, bool disable_screensaver) {
#ifdef _WIN32
// Clean up properly on Ctrl+C on Windows
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
@ -146,7 +146,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) {
}
#endif // _WIN32
if (!video_playback) {
if (!display) {
return;
}
@ -345,7 +345,6 @@ scrcpy(struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
@ -386,26 +385,24 @@ scrcpy(struct scrcpy_options *options) {
goto end;
}
// playback implies capture
assert(!options->video_playback || options->video);
assert(!options->audio_playback || options->audio);
if (options->video_playback) {
if (options->display) {
sdl_set_hints(options->render_driver);
}
// Initialize SDL video in addition if display is enabled
if (options->display) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
}
if (options->audio_playback) {
if (SDL_Init(SDL_INIT_AUDIO)) {
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end;
}
}
sdl_configure(options->video_playback, options->disable_screensaver);
sdl_configure(options->display, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling
bool connected;
@ -431,9 +428,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL;
// control implies video playback
assert(!options->control || options->video_playback);
if (options->control) {
if (options->display && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
@ -442,13 +437,11 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
if (options->video) {
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
.on_ended = sc_video_demuxer_on_ended,
};
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
}
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
.on_ended = sc_video_demuxer_on_ended,
};
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
if (options->audio) {
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
@ -458,8 +451,8 @@ scrcpy(struct scrcpy_options *options) {
&audio_demuxer_cbs, options);
}
bool needs_video_decoder = options->video_playback;
bool needs_audio_decoder = options->audio_playback;
bool needs_video_decoder = options->display;
bool needs_audio_decoder = options->audio && options->display;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
#endif
@ -479,8 +472,8 @@ scrcpy(struct scrcpy_options *options) {
.on_ended = sc_recorder_on_ended,
};
if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->video,
options->audio, &recorder_cbs, NULL)) {
options->record_format, options->audio,
&recorder_cbs, NULL)) {
goto end;
}
recorder_initialized = true;
@ -490,10 +483,8 @@ scrcpy(struct scrcpy_options *options) {
}
recorder_started = true;
if (options->video) {
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
}
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
if (options->audio) {
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
&s->recorder.audio_packet_sink);
@ -639,12 +630,23 @@ aoa_hid_end:
}
controller_started = true;
controller = &s->controller;
if (options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
}
// There is a controller if and only if control is enabled
assert(options->control == !!controller);
if (options->video_playback) {
if (options->display) {
const char *window_title =
options->window_title ? options->window_title : info->device_name;
@ -670,6 +672,11 @@ aoa_hid_end:
.start_fps_counter = options->start_fps_counter,
};
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
@ -678,19 +685,14 @@ aoa_hid_end:
src = &s->display_buffer.frame_source;
}
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
if (options->audio_playback) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
if (options->audio) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
}
}
#ifdef HAVE_V4L2
@ -712,15 +714,12 @@ aoa_hid_end:
}
#endif
// Now that the header values have been consumed, the socket(s) will
// receive the stream(s). Start the demuxer(s).
if (options->video) {
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
// now we consumed the header values, the socket receives the video stream
// start the video demuxer
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) {
@ -729,18 +728,6 @@ aoa_hid_end:
audio_demuxer_started = true;
}
// If the device screen is to be turned off, send the control message after
// everything is set up
if (options->control && options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
ret = event_loop(s);
LOGD("quit...");

View File

@ -56,7 +56,6 @@ static void
set_window_size(struct sc_screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
}
@ -240,6 +239,35 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
}
}
static bool
create_texture(struct sc_screen *screen) {
SDL_Renderer *renderer = screen->renderer;
struct sc_size size = screen->frame_size;
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
SDL_TEXTUREACCESS_STREAMING,
size.width, size.height);
if (!texture) {
LOGE("Could not create texture: %s", SDL_GetError());
return false;
}
if (screen->mipmaps) {
struct sc_opengl *gl = &screen->gl;
SDL_GL_BindTexture(texture, NULL, NULL);
// 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);
SDL_GL_UnbindTexture(texture);
}
screen->texture = texture;
return true;
}
// render the texture to the renderer
//
// Set the update_content_rect flag if the window or content size may have
@ -250,11 +278,35 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
sc_screen_update_content_rect(screen);
}
enum sc_display_result res =
sc_display_render(&screen->display, &screen->rect, screen->rotation);
(void) res; // any error already logged
SDL_RenderClear(screen->renderer);
if (screen->rotation == 0) {
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
} else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - screen->rotation) % 4;
double angle = 90 * cw_rotation;
SDL_Rect *dstrect = NULL;
SDL_Rect rect;
if (screen->rotation & 1) {
rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
rect.w = screen->rect.h;
rect.h = screen->rect.w;
dstrect = &rect;
} else {
assert(screen->rotation == 2);
dstrect = &screen->rect;
}
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
angle, NULL, 0);
}
SDL_RenderPresent(screen->renderer);
}
#if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND
#endif
@ -360,7 +412,6 @@ sc_screen_init(struct sc_screen *screen,
screen->has_frame = false;
screen->fullscreen = false;
screen->maximized = false;
screen->minimized = false;
screen->mouse_capture_key_pressed = 0;
screen->req.x = params->window_x;
@ -402,11 +453,46 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter;
}
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
if (!ok) {
screen->renderer = SDL_CreateRenderer(screen->window, -1,
SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window;
}
SDL_RendererInfo renderer_info;
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info);
const char *renderer_name = r ? NULL : renderer_info.name;
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
screen->mipmaps = false;
// starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) {
struct sc_opengl *gl = &screen->gl;
sc_opengl_init(gl);
LOGI("OpenGL version: %s", gl->version);
if (params->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");
screen->mipmaps = true;
} else {
LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required)");
}
} else {
LOGI("Trilinear filtering disabled");
}
} else if (params->mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
}
SDL_Surface *icon = scrcpy_icon_load();
if (icon) {
SDL_SetWindowIcon(screen->window, icon);
@ -418,7 +504,7 @@ sc_screen_init(struct sc_screen *screen,
screen->frame = av_frame_alloc();
if (!screen->frame) {
LOG_OOM();
goto error_destroy_display;
goto error_destroy_renderer;
}
struct sc_input_manager_params im_params = {
@ -453,8 +539,8 @@ sc_screen_init(struct sc_screen *screen,
return true;
error_destroy_display:
sc_display_destroy(&screen->display);
error_destroy_renderer:
SDL_DestroyRenderer(screen->renderer);
error_destroy_window:
SDL_DestroyWindow(screen->window);
error_destroy_fps_counter:
@ -510,8 +596,11 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG
assert(!screen->open);
#endif
sc_display_destroy(&screen->display);
av_frame_free(&screen->frame);
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb);
@ -533,11 +622,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
if (!screen->fullscreen && !screen->maximized) {
resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once
// fullscreen/maximized/minimized are disabled
// fullscreen and maximized are disabled
screen->windowed_content_size = screen->content_size;
screen->resize_pending = true;
}
@ -549,7 +638,6 @@ static void
apply_pending_resize(struct sc_screen *screen) {
assert(!screen->fullscreen);
assert(!screen->maximized);
assert(!screen->minimized);
if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size,
screen->content_size);
@ -579,6 +667,7 @@ static bool
sc_screen_init_size(struct sc_screen *screen) {
// Before first frame
assert(!screen->has_frame);
assert(!screen->texture);
// The requested size is passed via screen->frame_size
@ -586,29 +675,48 @@ sc_screen_init_size(struct sc_screen *screen) {
get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size;
enum sc_display_result res =
sc_display_set_texture_size(&screen->display, screen->frame_size);
return res != SC_DISPLAY_RESULT_ERROR;
LOGI("Initial texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
return create_texture(screen);
}
// recreate the texture and resize the window if the frame size has changed
static enum sc_display_result
static bool
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) {
return SC_DISPLAY_RESULT_OK;
if (screen->frame_size.width != new_frame_size.width
|| screen->frame_size.height != new_frame_size.height) {
// frame dimension changed, destroy texture
SDL_DestroyTexture(screen->texture);
screen->frame_size = new_frame_size;
struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen);
LOGI("New texture: %" PRIu16 "x%" PRIu16,
screen->frame_size.width, screen->frame_size.height);
return create_texture(screen);
}
// frame dimension changed
screen->frame_size = new_frame_size;
return true;
}
struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation);
set_content_size(screen, new_content_size);
// write the frame into the texture
static void
update_texture(struct sc_screen *screen, const AVFrame *frame) {
SDL_UpdateYUVTexture(screen->texture, NULL,
frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]);
sc_screen_update_content_rect(screen);
return sc_display_set_texture_size(&screen->display, screen->frame_size);
if (screen->mipmaps) {
SDL_GL_BindTexture(screen->texture, NULL, NULL);
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(screen->texture);
}
}
static bool
@ -620,23 +728,10 @@ sc_screen_update_frame(struct sc_screen *screen) {
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
struct sc_size new_frame_size = {frame->width, frame->height};
enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
if (res == SC_DISPLAY_RESULT_ERROR) {
if (!prepare_for_frame(screen, new_frame_size)) {
return false;
}
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
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;
}
update_texture(screen, frame);
if (!screen->has_frame) {
screen->has_frame = true;
@ -662,7 +757,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
}
screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
if (!screen->fullscreen && !screen->maximized) {
apply_pending_resize(screen);
}
@ -672,7 +767,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
void
sc_screen_resize_to_fit(struct sc_screen *screen) {
if (screen->fullscreen || screen->maximized || screen->minimized) {
if (screen->fullscreen || screen->maximized) {
return;
}
@ -696,7 +791,7 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
if (screen->fullscreen || screen->minimized) {
if (screen->fullscreen) {
return;
}
@ -717,7 +812,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
}
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
@ -753,9 +848,6 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true;
break;
case SDL_WINDOWEVENT_MINIMIZED:
screen->minimized = true;
break;
case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling
@ -766,7 +858,6 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
break;
}
screen->maximized = false;
screen->minimized = false;
apply_pending_resize(screen);
sc_screen_render(screen, true);
break;

View File

@ -9,7 +9,6 @@
#include "controller.h"
#include "coords.h"
#include "display.h"
#include "fps_counter.h"
#include "frame_buffer.h"
#include "input_manager.h"
@ -25,7 +24,6 @@ struct sc_screen {
bool open; // track the open/close state to assert correct behavior
#endif
struct sc_display display;
struct sc_input_manager im;
struct sc_frame_buffer fb;
struct sc_fps_counter fps_counter;
@ -41,6 +39,9 @@ struct sc_screen {
} req;
SDL_Window *window;
SDL_Renderer *renderer;
SDL_Texture *texture;
struct sc_opengl gl;
struct sc_size frame_size;
struct sc_size content_size; // rotated frame_size
@ -56,7 +57,7 @@ struct sc_screen {
bool has_frame;
bool fullscreen;
bool maximized;
bool minimized;
bool mipmaps;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key.
@ -136,7 +137,7 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
// react to SDL events
// If this function returns false, scrcpy must exit with an error.
bool
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
// convert point from window coordinates to frame coordinates
// x and y are expressed in pixels

View File

@ -226,16 +226,12 @@ execute_server(struct sc_server *server,
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
if (!params->video) {
ADD_PARAM("video=false");
}
if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
}
if (!params->audio) {
ADD_PARAM("audio=false");
}
if (params->audio_bit_rate) {
} else if (params->audio_bit_rate) {
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
}
if (params->video_codec != SC_CODEC_H264) {
@ -467,7 +463,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial;
assert(serial);
bool video = server->params.video;
bool audio = server->params.audio;
bool control = server->params.control;
@ -475,12 +470,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) {
if (video) {
video_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
if (audio) {
@ -511,45 +503,35 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
unsigned attempts = 100;
sc_tick delay = SC_TICK_FROM_MS(100);
sc_socket first_socket = connect_to_server(server, attempts, delay,
tunnel_host, tunnel_port);
if (first_socket == SC_SOCKET_NONE) {
video_socket = connect_to_server(server, attempts, delay, tunnel_host,
tunnel_port);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
if (video) {
video_socket = first_socket;
}
if (audio) {
if (!video) {
audio_socket = first_socket;
} else {
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
}
if (control) {
if (!video && !audio) {
control_socket = first_socket;
} else {
control_socket = net_socket();
if (control_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, control_socket,
tunnel_host, tunnel_port);
if (!ok) {
goto fail;
}
// we know that the device is listening, we don't need several
// attempts
control_socket = net_socket();
if (control_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, control_socket,
tunnel_host, tunnel_port);
if (!ok) {
goto fail;
}
}
}
@ -558,17 +540,13 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
sc_socket first_socket = video ? video_socket
: audio ? audio_socket
: control_socket;
// The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, first_socket, info);
bool ok = device_read_info(&server->intr, video_socket, info);
if (!ok) {
goto fail;
}
assert(!video || video_socket != SC_SOCKET_NONE);
assert(video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE);
@ -952,11 +930,8 @@ run_server(void *data) {
sc_mutex_unlock(&server->mutex);
// Interrupt sockets to wake up socket blocking calls on the server
if (server->video_socket != SC_SOCKET_NONE) {
// There is no video_socket if --no-video is set
net_interrupt(server->video_socket);
}
assert(server->video_socket != SC_SOCKET_NONE);
net_interrupt(server->video_socket);
if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set

View File

@ -41,7 +41,6 @@ struct sc_server_params {
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool video;
bool audio;
bool show_touches;
bool stay_awake;

View File

@ -217,18 +217,6 @@ static void test_get_ip_multiline_second_ok(void) {
free(ip);
}
static void test_get_ip_multiline_second_ok_without_cr(void) {
char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src "
"10.0.0.3\n"
"192.168.1.0/24 dev wlan0 proto kernel scope link src "
"192.168.1.3\n";
char *ip = sc_adb_parse_device_ip(ip_route);
assert(ip);
assert(!strcmp(ip, "192.168.1.3"));
free(ip);
}
static void test_get_ip_no_wlan(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34\r\r\n";
@ -271,7 +259,6 @@ int main(int argc, char *argv[]) {
test_get_ip_single_line_with_trailing_space();
test_get_ip_multiline_first_ok();
test_get_ip_multiline_second_ok();
test_get_ip_multiline_second_ok_without_cr();
test_get_ip_no_wlan();
test_get_ip_no_wlan_without_eol();
test_get_ip_truncated();

View File

@ -53,7 +53,7 @@ static void test_options(void) {
"--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off"
// "--no-playback" is not compatible with "--fulscreen"
// "--no-display" is not compatible with "--fulscreen"
"--port", "1234:1236",
"--push-target", "/sdcard/Movies",
"--record", "file",
@ -108,8 +108,8 @@ static void test_options2(void) {
char *argv[] = {
"scrcpy",
"--no-control",
"--no-playback",
"--record", "file.mp4", // cannot enable --no-playback without recording
"--no-display",
"--record", "file.mp4", // cannot enable --no-display without recording
};
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
@ -117,8 +117,7 @@ static void test_options2(void) {
const struct scrcpy_options *opts = &args.opts;
assert(!opts->control);
assert(!opts->video_playback);
assert(!opts->audio_playback);
assert(!opts->display);
assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == SC_RECORD_FORMAT_MP4);
}

View File

@ -16,6 +16,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@ -16,6 +16,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64'
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@ -24,23 +24,6 @@ To disable audio:
scrcpy --no-audio
```
To disable only the audio playback, see [no playback](video.md#no-playback).
## Audio only
To play audio only, disable the video:
```
scrcpy --no-video
```
Without video, the audio latency is typically not criticial, so it might be
interesting to add [buffering](#buffering) to minimize glitches:
```
scrcpy --no-video --audio-buffer=200
```
## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac`

View File

@ -13,18 +13,12 @@ To record only the video:
scrcpy --no-audio --record=file.mp4
```
To record only the audio:
_It is currently not possible to record only the audio._
To disable mirroring while recording:
```bash
scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record-file=file.aac
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
```
To disable playback while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy --no-display --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```

View File

@ -35,7 +35,7 @@ To start `scrcpy` using a v4l2 sink:
```bash
scrcpy --v4l2-sink=/dev/videoN
scrcpy --v4l2-sink=/dev/videoN --no-video-playback # disable playback window
scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window
```
(replace `N` with the device ID, check with `ls /dev/video*`)

View File

@ -159,37 +159,17 @@ scrcpy --display-buffer=50 --v4l2-buffer=300
```
## No playback
## No display
It is possible to capture an Android device without playing video or audio on
the computer. This option is useful when [recording](recording.md) or when
It is possible to capture an Android device without displaying a mirroring
window. This option is available if either [recording](recording.md) or
[v4l2](#video4linux) is enabled:
```bash
scrcpy --v4l2-sink=/dev/video2 --no-playback
scrcpy --record=file.mkv --no-playback
scrcpy --v4l2-sink=/dev/video2 --no-display
scrcpy --record=file.mkv --no-display
```
It is also possible to disable video and audio playback separately:
```bash
# Send video to V4L2 sink without playing it, but keep audio playback
scrcpy --v4l2-sink=/dev/video2 --no-video-playback
# Record both video and audio, but only play video
scrcpy --record=file.mkv --no-audio-playback
```
## No video
To disable video forwarding completely, so that only audio is forwarded:
```
scrcpy --no-video
```
## Video4Linux
See the dedicated [Video4Linux](v4l2.md) page.

View File

@ -94,10 +94,11 @@ dist-win32: build-server build-win32
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
@ -112,10 +113,11 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"

View File

@ -14,8 +14,8 @@ set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.0
PLATFORM=${ANDROID_PLATFORM:-33}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
PLATFORM=${ANDROID_PLATFORM:-23}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-23.0.3}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -43,6 +43,17 @@ public final class BuildConfig {
}
EOF
STUBS_DIR="$BUILD_DIR/stubs"
rm -rf "$STUBS_DIR"
mkdir -p "$STUBS_DIR"
echo "Generating SDK stubs..."
cd "$SERVER_DIR/src/main/stubs"
javac -bootclasspath "$ANDROID_JAR" \
-d "$STUBS_DIR" \
-source 1.8 -target 1.8 \
android/content/*
cd -
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
@ -52,7 +63,7 @@ cd "$SERVER_DIR/src/main/aidl"
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-cp "$LAMBDA_JAR:$GEN_DIR:$STUBS_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \

View File

@ -1,26 +0,0 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IDisplayFoldListener
{
/** Called when the foldedness of a display changes */
void onDisplayFoldChanged(int displayId, boolean folded);
}

View File

@ -1,16 +1,7 @@
package com.genymobile.scrcpy;
public interface AsyncProcessor {
interface TerminationListener {
/**
* Notify processor termination
*
* @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server.
*/
void onTerminated(boolean fatalError);
}
void start(TerminationListener listener);
void start();
void stop();
void join() throws InterruptedException;
}

View File

@ -5,6 +5,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
@ -14,16 +15,15 @@ import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
public final class AudioCapture {
public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX;
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
private AudioRecord recorder;
@ -38,23 +38,38 @@ public final class AudioCapture {
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setEncoding(FORMAT);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
private static Method setBuilderContext;
@TargetApi(23)
private static void setBuilderContext(AudioRecord.Builder builder, Context context) {
try {
if (setBuilderContext == null) {
setBuilderContext = AudioRecord.Builder.class.getMethod("setContext", Context.class);
}
setBuilderContext.invoke(builder, context);
} catch (Exception e) {
Ln.e("Could not call AudioRecord.Builder.setContext() method");
//throw new RuntimeException(e);
}
}
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= 31) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
setBuilderContext(builder, FakeContext.get());
}
builder.setAudioSource(SOURCE);
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
return builder.build();
@ -88,8 +103,8 @@ public final class AudioCapture {
} catch (UnsupportedOperationException e) {
if (attempts == 0) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
"scrcpy.");
throw new AudioCaptureForegroundException();
} else {
Ln.d("Failed to start audio capture, retrying...");
@ -99,19 +114,12 @@ public final class AudioCapture {
}
private void startRecording() {
try {
recorder = createAudioRecord();
} catch (NullPointerException e) {
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
// - <https://github.com/Genymobile/scrcpy/issues/3805>
// - <https://github.com/Genymobile/scrcpy/pull/3862>
recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder = createAudioRecord();
recorder.startRecording();
}
public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT == 30) {
startWorkaroundAndroid11();
try {
tryStartRecording(3, 100);
@ -130,7 +138,21 @@ public final class AudioCapture {
}
}
@TargetApi(Build.VERSION_CODES.N)
private static Method getTimestampMethod;
private static int getRecorderTimestamp(AudioRecord recorder, AudioTimestamp timestamp) {
try {
if (getTimestampMethod == null) {
getTimestampMethod = AudioRecord.class.getMethod("getTimestamp", AudioTimestamp.class, int.class);
}
return (int) getTimestampMethod.invoke(recorder, timestamp, 0);
} catch (Exception e) {
Ln.e("Could not call AudioRecord.getTimestamp() method");
return AudioRecord.ERROR;
}
}
@TargetApi(24)
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, size);
if (r <= 0) {
@ -139,7 +161,7 @@ public final class AudioCapture {
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
int ret = getRecorderTimestamp(recorder, timestamp);
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
} else {

View File

@ -84,7 +84,7 @@ public final class AudioEncoder implements AsyncProcessor {
return format;
}
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(24)
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@ -114,29 +114,21 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
boolean fatalError = false;
try {
encode();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
} catch (ConfigurationException | AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
fatalError = true;
} finally {
Ln.d("Audio encoder stopped");
listener.onTerminated(fatalError);
}
});
thread.start();
}
@Override
public void stop() {
if (thread != null) {
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
@ -144,7 +136,6 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
@ -168,7 +159,7 @@ public final class AudioEncoder implements AsyncProcessor {
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT < 30) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
@ -299,7 +290,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
private class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N)
@TargetApi(24)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {

View File

@ -1,7 +1,6 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import android.os.Build;
import java.io.IOException;
import java.nio.ByteBuffer;
@ -20,12 +19,6 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
private void record() throws IOException, AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@ -53,33 +46,27 @@ public final class AudioRawRecorder implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
boolean fatalError = false;
try {
record();
} catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio recording error", e);
fatalError = true;
} finally {
Ln.d("Audio recorder stopped");
listener.onTerminated(fatalError);
}
});
thread.start();
}
@Override
public void stop() {
if (thread != null) {
thread.interrupt();
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();

View File

@ -84,8 +84,7 @@ public class Controller implements AsyncProcessor {
}
}
@Override
public void start(TerminationListener listener) {
public void start() {
thread = new Thread(() -> {
try {
control();
@ -93,14 +92,12 @@ public class Controller implements AsyncProcessor {
// this is expected on close
} finally {
Ln.d("Controller stopped");
listener.onTerminated(true);
}
});
thread.start();
sender.start();
}
@Override
public void stop() {
if (thread != null) {
thread.interrupt();
@ -108,7 +105,6 @@ public class Controller implements AsyncProcessor {
sender.stop();
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
@ -377,8 +373,8 @@ public class Controller implements AsyncProcessor {
private void getClipboard(int copyKey) {
// On Android >= 7, press the COPY or CUT key if requested
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
int key = copyKey == ControlMessage.COPY_KEY_COPY ? 278 : 277;
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
}
@ -401,8 +397,8 @@ public class Controller implements AsyncProcessor {
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
if (paste && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
device.pressReleaseKeycode(279, Device.INJECT_MODE_ASYNC);
}
if (sequence != ControlMessage.SEQUENCE_INVALID) {

View File

@ -41,7 +41,7 @@ public final class DesktopConnection implements Closeable {
controlInputStream = null;
controlOutputStream = null;
}
videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
videoFd = videoSocket.getFileDescriptor();
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
}
@ -60,43 +60,32 @@ public final class DesktopConnection implements Closeable {
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
}
public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte)
throws IOException {
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(scid);
LocalSocket firstSocket = null;
LocalSocket videoSocket = null;
LocalSocket audioSocket = null;
LocalSocket controlSocket = null;
try {
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
if (video) {
videoSocket = localServerSocket.accept();
firstSocket = videoSocket;
LocalServerSocket localServerSocket = new LocalServerSocket(socketName);
try {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (audio) {
audioSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = audioSocket;
}
}
if (control) {
controlSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = controlSocket;
}
}
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
firstSocket.getOutputStream().write(0);
}
} finally {
localServerSocket.close();
}
} else {
if (video) {
videoSocket = connect(socketName);
}
videoSocket = connect(socketName);
if (audio) {
audioSocket = connect(socketName);
}
@ -120,22 +109,10 @@ public final class DesktopConnection implements Closeable {
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
}
private LocalSocket getFirstSocket() {
if (videoSocket != null) {
return videoSocket;
}
if (audioSocket != null) {
return audioSocket;
}
return controlSocket;
}
public void close() throws IOException {
if (videoSocket != null) {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
}
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
if (audioSocket != null) {
audioSocket.shutdownInput();
audioSocket.shutdownOutput();
@ -156,8 +133,7 @@ public final class DesktopConnection implements Closeable {
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
FileDescriptor fd = getFirstSocket().getFileDescriptor();
IO.writeFully(fd, buffer, 0, buffer.length);
IO.writeFully(videoFd, buffer, 0, buffer.length);
}
public FileDescriptor getVideoFd() {

View File

@ -12,7 +12,6 @@ import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
@ -36,10 +35,6 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
@ -51,7 +46,6 @@ public final class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@ -99,26 +93,6 @@ public final class Device {
}
}, displayId);
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
synchronized (Device.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
options.getMaxSize(), options.getLockVideoOrientation());
// notify
if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded);
}
}
}
});
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
@ -150,7 +124,7 @@ public final class Device {
}
// main display or any display on Android >= Q
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= 29;
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
@ -199,7 +173,7 @@ public final class Device {
}
public static boolean supportsInputEvents(int displayId) {
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
return displayId == 0 || Build.VERSION.SDK_INT >= 29;
}
public boolean supportsInputEvents() {
@ -250,10 +224,6 @@ public final class Device {
this.rotationListener = rotationListener;
}
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
@ -307,7 +277,7 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT >= 29) {
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {

View File

@ -26,15 +26,13 @@ public final class FakeContext extends ContextWrapper {
return PACKAGE_NAME;
}
@Override
public String getOpPackageName() {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@Override
@TargetApi(31)
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
AttributionSource.Builder builder = new AttributionSource.Builder(0);
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}

View File

@ -3,13 +3,11 @@ package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.List;
import java.util.Locale;
public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int scid = -1; // 31-bit non-negative value, or -1
private boolean video = true;
private boolean audio = true;
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
@ -48,86 +46,166 @@ public class Options {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getScid() {
return scid;
}
public boolean getVideo() {
return video;
public void setScid(int scid) {
this.scid = scid;
}
public boolean getAudio() {
return audio;
}
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public VideoCodec getVideoCodec() {
return videoCodec;
}
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() {
return audioCodec;
}
public void setAudioCodec(AudioCodec audioCodec) {
this.audioCodec = audioCodec;
}
public int getVideoBitRate() {
return videoBitRate;
}
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() {
return audioBitRate;
}
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
}
public int getMaxFps() {
return maxFps;
}
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getControl() {
return control;
}
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() {
return showTouches;
}
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() {
return stayAwake;
}
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
}
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
}
public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions;
}
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() {
return videoEncoder;
}
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() {
return audioEncoder;
}
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose;
}
@ -136,207 +214,79 @@ public class Options {
return clipboardAutosync;
}
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
public boolean getDownsizeOnError() {
return downsizeOnError;
}
public void setDownsizeOnError(boolean downsizeOnError) {
this.downsizeOnError = downsizeOnError;
}
public boolean getCleanup() {
return cleanup;
}
public void setCleanup(boolean cleanup) {
this.cleanup = cleanup;
}
public boolean getPowerOn() {
return powerOn;
}
public void setPowerOn(boolean powerOn) {
this.powerOn = powerOn;
}
public boolean getListEncoders() {
return listEncoders;
}
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() {
return listDisplays;
}
public void setListDisplays(boolean listDisplays) {
this.listDisplays = listDisplays;
}
public boolean getSendDeviceMeta() {
return sendDeviceMeta;
}
public void setSendDeviceMeta(boolean sendDeviceMeta) {
this.sendDeviceMeta = sendDeviceMeta;
}
public boolean getSendFrameMeta() {
return sendFrameMeta;
}
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getSendDummyByte() {
return sendDummyByte;
}
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getSendCodecMeta() {
return sendCodecMeta;
}
@SuppressWarnings("MethodLength")
public static Options parse(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.scid = scid;
break;
case "log_level":
options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
break;
case "video":
options.video = Boolean.parseBoolean(value);
break;
case "audio":
options.audio = Boolean.parseBoolean(value);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.videoCodec = videoCodec;
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.audioCodec = audioCodec;
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;
case "video_bit_rate":
options.videoBitRate = Integer.parseInt(value);
break;
case "audio_bit_rate":
options.audioBitRate = Integer.parseInt(value);
break;
case "max_fps":
options.maxFps = Integer.parseInt(value);
break;
case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value);
break;
case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value);
break;
case "crop":
options.crop = parseCrop(value);
break;
case "control":
options.control = Boolean.parseBoolean(value);
break;
case "display_id":
options.displayId = Integer.parseInt(value);
break;
case "show_touches":
options.showTouches = Boolean.parseBoolean(value);
break;
case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value);
break;
case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value);
break;
case "audio_codec_options":
options.audioCodecOptions = CodecOption.parse(value);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.videoEncoder = value;
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.audioEncoder = value;
}
case "power_off_on_close":
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
break;
case "clipboard_autosync":
options.clipboardAutosync = Boolean.parseBoolean(value);
break;
case "downsize_on_error":
options.downsizeOnError = Boolean.parseBoolean(value);
break;
case "cleanup":
options.cleanup = Boolean.parseBoolean(value);
break;
case "power_on":
options.powerOn = Boolean.parseBoolean(value);
break;
case "list_encoders":
options.listEncoders = Boolean.parseBoolean(value);
break;
case "list_displays":
options.listDisplays = Boolean.parseBoolean(value);
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;
case "send_frame_meta":
options.sendFrameMeta = Boolean.parseBoolean(value);
break;
case "send_dummy_byte":
options.sendDummyByte = Boolean.parseBoolean(value);
break;
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendCodecMeta = false;
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
public void setSendCodecMeta(boolean sendCodecMeta) {
this.sendCodecMeta = sendCodecMeta;
}
}

View File

@ -16,7 +16,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor {
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@ -26,7 +26,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean resetCapture = new AtomicBoolean();
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final Device device;
private final Streamer streamer;
@ -39,9 +39,6 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private boolean firstFrameSent;
private int consecutiveErrors;
private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean();
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) {
this.device = device;
@ -53,27 +50,21 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
this.downsizeOnError = downsizeOnError;
}
@Override
public void onFoldChanged(int displayId, boolean folded) {
resetCapture.set(true);
}
@Override
public void onRotationChanged(int rotation) {
resetCapture.set(true);
rotationChanged.set(true);
}
private boolean consumeResetCapture() {
return resetCapture.getAndSet(false);
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException {
public void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
@ -121,7 +112,6 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
} finally {
mediaCodec.release();
device.setRotationListener(null);
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
}
}
@ -173,17 +163,12 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false;
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeResetCapture() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (consumeResetCapture()) {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
@ -208,7 +193,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
}
}
return !eof && alive;
return !eof;
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@ -267,7 +252,7 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
private static IBinder createDisplay() {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S"
boolean secure = Build.VERSION.SDK_INT < 30 || (Build.VERSION.SDK_INT == 30 && !"S"
.equals(Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure);
}
@ -282,38 +267,4 @@ public class ScreenEncoder implements Device.RotationListener, Device.FoldListen
SurfaceControl.closeTransaction();
}
}
@Override
public void start(TerminationListener listener) {
thread = new Thread(() -> {
try {
streamScreen();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
}
} finally {
Ln.d("Screen streaming stopped");
listener.onTerminated(true);
}
});
thread.start();
}
@Override
public void stop() {
if (thread != null) {
stopped.set(true);
}
}
@Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
}

View File

@ -1,43 +1,16 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.os.BatteryManager;
import android.os.Build;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class Server {
private static class Completion {
private int running;
private boolean fatalError;
Completion(int running) {
this.running = running;
}
synchronized void addCompleted(boolean fatalError) {
--running;
if (fatalError) {
this.fatalError = true;
}
if (running == 0 || this.fatalError) {
notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
}
}
}
private Server() {
// not instantiable
}
@ -95,7 +68,6 @@ public final class Server {
int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
@ -116,14 +88,13 @@ public final class Server {
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill the application context for the AudioRecord to work.
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (audio && Build.VERSION.SDK_INT == 30) {
Workarounds.fillAppContext();
}
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
try {
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) {
connection.sendDeviceMeta(Device.getDeviceName());
}
@ -148,23 +119,26 @@ public final class Server {
asyncProcessors.add(audioRecorder);
}
if (video) {
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(screenEncoder);
}
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
Completion completion = new Completion(asyncProcessors.size());
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.start((fatalError) -> {
completion.addCompleted(fatalError);
});
asyncProcessor.start();
}
completion.await();
try {
// synchronous
screenEncoder.streamScreen();
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
}
}
} finally {
Ln.d("Screen streaming stopped");
initThread.interrupt();
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop();
@ -178,8 +152,6 @@ public final class Server {
} catch (InterruptedException e) {
// ignore
}
connection.close();
}
}
@ -189,12 +161,203 @@ public final class Server {
return thread;
}
@SuppressWarnings("MethodLength")
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.setScid(scid);
break;
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "audio":
boolean audio = Boolean.parseBoolean(value);
options.setAudio(audio);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.setVideoCodec(videoCodec);
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.setAudioCodec(audioCodec);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "video_bit_rate":
int videoBitRate = Integer.parseInt(value);
options.setVideoBitRate(videoBitRate);
break;
case "audio_bit_rate":
int audioBitRate = Integer.parseInt(value);
options.setAudioBitRate(audioBitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockVideoOrientation = Integer.parseInt(value);
options.setLockVideoOrientation(lockVideoOrientation);
break;
case "tunnel_forward":
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "video_codec_options":
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
options.setVideoCodecOptions(videoCodecOptions);
break;
case "audio_codec_options":
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
options.setAudioCodecOptions(audioCodecOptions);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.setVideoEncoder(value);
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.setAudioEncoder(value);
}
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
break;
case "clipboard_autosync":
boolean clipboardAutosync = Boolean.parseBoolean(value);
options.setClipboardAutosync(clipboardAutosync);
break;
case "downsize_on_error":
boolean downsizeOnError = Boolean.parseBoolean(value);
options.setDownsizeOnError(downsizeOnError);
break;
case "cleanup":
boolean cleanup = Boolean.parseBoolean(value);
options.setCleanup(cleanup);
break;
case "power_on":
boolean powerOn = Boolean.parseBoolean(value);
options.setPowerOn(powerOn);
break;
case "list_encoders":
boolean listEncoders = Boolean.parseBoolean(value);
options.setListEncoders(listEncoders);
break;
case "list_displays":
boolean listDisplays = Boolean.parseBoolean(value);
options.setListDisplays(listDisplays);
break;
case "send_device_meta":
boolean sendDeviceMeta = Boolean.parseBoolean(value);
options.setSendDeviceMeta(sendDeviceMeta);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "send_dummy_byte":
boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte);
break;
case "send_codec_meta":
boolean sendCodecMeta = Boolean.parseBoolean(value);
options.setSendCodecMeta(sendCodecMeta);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.setSendDeviceMeta(false);
options.setSendFrameMeta(false);
options.setSendDummyByte(false);
options.setSendCodecMeta(false);
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
});
Options options = Options.parse(args);
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());

View File

@ -34,7 +34,7 @@ public final class Settings {
}
public static String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT <= 30) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
@ -47,7 +47,7 @@ public final class Settings {
}
public static void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT <= 30) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
@ -60,7 +60,7 @@ public final class Settings {
}
public static String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT <= 30) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);

View File

@ -7,7 +7,7 @@ public enum VideoCodec implements Codec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
@SuppressLint("InlinedApi") // introduced in API 21
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
AV1(0x00_61_76_31, "av1", "video/av01");
private final int id; // 4-byte ASCII representation of the name
private final String name;

View File

@ -1,22 +1,13 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
import android.content.AttributionSource;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.os.Build;
import android.os.Looper;
import android.os.Parcel;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
@ -104,140 +95,4 @@ public final class Workarounds {
Ln.d("Could not fill app context: " + throwable.getMessage());
}
}
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//
// This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses
// reflections to initialize it like the normal constructor do (or the `AudioRecord.Builder.build()` method do).
// As a result, the modified code was not executed.
try {
// AudioRecord audioRecord = new AudioRecord(0L);
Constructor<AudioRecord> audioRecordConstructor = AudioRecord.class.getDeclaredConstructor(long.class);
audioRecordConstructor.setAccessible(true);
AudioRecord audioRecord = audioRecordConstructor.newInstance(0L);
// audioRecord.mRecordingState = RECORDSTATE_STOPPED;
Field mRecordingStateField = AudioRecord.class.getDeclaredField("mRecordingState");
mRecordingStateField.setAccessible(true);
mRecordingStateField.set(audioRecord, AudioRecord.RECORDSTATE_STOPPED);
Looper looper = Looper.myLooper();
if (looper == null) {
looper = Looper.getMainLooper();
}
// audioRecord.mInitializationLooper = looper;
Field mInitializationLooperField = AudioRecord.class.getDeclaredField("mInitializationLooper");
mInitializationLooperField.setAccessible(true);
mInitializationLooperField.set(audioRecord, looper);
// Create `AudioAttributes` with fixed capture preset
int capturePreset = source;
AudioAttributes.Builder audioAttributesBuilder = new AudioAttributes.Builder();
Method setInternalCapturePresetMethod = AudioAttributes.Builder.class.getMethod("setInternalCapturePreset", int.class);
setInternalCapturePresetMethod.invoke(audioAttributesBuilder, capturePreset);
AudioAttributes attributes = audioAttributesBuilder.build();
// audioRecord.mAudioAttributes = attributes;
Field mAudioAttributesField = AudioRecord.class.getDeclaredField("mAudioAttributes");
mAudioAttributesField.setAccessible(true);
mAudioAttributesField.set(audioRecord, attributes);
// audioRecord.audioParamCheck(capturePreset, sampleRate, encoding);
Method audioParamCheckMethod = AudioRecord.class.getDeclaredMethod("audioParamCheck", int.class, int.class, int.class);
audioParamCheckMethod.setAccessible(true);
audioParamCheckMethod.invoke(audioRecord, capturePreset, sampleRate, encoding);
// audioRecord.mChannelCount = channels
Field mChannelCountField = AudioRecord.class.getDeclaredField("mChannelCount");
mChannelCountField.setAccessible(true);
mChannelCountField.set(audioRecord, channels);
// audioRecord.mChannelMask = channelMask
Field mChannelMaskField = AudioRecord.class.getDeclaredField("mChannelMask");
mChannelMaskField.setAccessible(true);
mChannelMaskField.set(audioRecord, channelMask);
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, encoding);
int bufferSizeInBytes = minBufferSize * 8;
// audioRecord.audioBuffSizeCheck(bufferSizeInBytes)
Method audioBuffSizeCheckMethod = AudioRecord.class.getDeclaredMethod("audioBuffSizeCheck", int.class);
audioBuffSizeCheckMethod.setAccessible(true);
audioBuffSizeCheckMethod.invoke(audioRecord, bufferSizeInBytes);
final int channelIndexMask = 0;
int[] sampleRateArray = new int[]{sampleRate};
int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE};
int initResult;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// private native final int native_setup(Object audiorecord_this,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// int buffSizeInBytes, int[] sessionId, String opPackageName,
// long nativeRecordInJavaObj);
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
int.class, int.class, int.class, int[].class, String.class, long.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, FakeContext.get().getOpPackageName(),
0L);
} else {
// Assume `context` is never `null`
AttributionSource attributionSource = FakeContext.get().getAttributionSource();
// Assume `attributionSource.getPackageName()` is never null
// ScopedParcelState attributionSourceState = attributionSource.asScopedParcelState()
Method asScopedParcelStateMethod = AttributionSource.class.getDeclaredMethod("asScopedParcelState");
asScopedParcelStateMethod.setAccessible(true);
try (AutoCloseable attributionSourceState = (AutoCloseable) asScopedParcelStateMethod.invoke(attributionSource)) {
Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel");
Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState);
// private native int native_setup(Object audiorecordThis,
// Object /*AudioAttributes*/ attributes,
// int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
// int buffSizeInBytes, int[] sessionId, @NonNull Parcel attributionSource,
// long nativeRecordInJavaObj, int maxSharedAudioHistoryMs);
Method nativeSetupMethod = AudioRecord.class.getDeclaredMethod("native_setup", Object.class, Object.class, int[].class, int.class,
int.class, int.class, int.class, int[].class, Parcel.class, long.class, int.class);
nativeSetupMethod.setAccessible(true);
initResult = (int) nativeSetupMethod.invoke(audioRecord, new WeakReference<AudioRecord>(audioRecord), attributes, sampleRateArray,
channelMask, channelIndexMask, audioRecord.getAudioFormat(), bufferSizeInBytes, session, attributionSourceParcel, 0L, 0);
}
}
if (initResult != AudioRecord.SUCCESS) {
Ln.e("Error code " + initResult + " when initializing native AudioRecord object.");
throw new RuntimeException("Cannot create AudioRecord");
}
// mSampleRate = sampleRate[0]
Field mSampleRateField = AudioRecord.class.getDeclaredField("mSampleRate");
mSampleRateField.setAccessible(true);
mSampleRateField.set(audioRecord, sampleRateArray[0]);
// audioRecord.mSessionId = session[0]
Field mSessionIdField = AudioRecord.class.getDeclaredField("mSessionId");
mSessionIdField.setAccessible(true);
mSessionIdField.set(audioRecord, session[0]);
// audioRecord.mState = AudioRecord.STATE_INITIALIZED
Field mStateField = AudioRecord.class.getDeclaredField("mState");
mStateField.setAccessible(true);
mStateField.set(audioRecord, AudioRecord.STATE_INITIALIZED);
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord.<init>.", e);
throw new RuntimeException("Cannot create AudioRecord");
}
}
}

View File

@ -51,7 +51,7 @@ public class ActivityManager {
return removeContentProviderExternalMethod;
}
@TargetApi(Build.VERSION_CODES.Q)
@TargetApi(29)
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();

View File

@ -26,7 +26,7 @@ public class ClipboardManager {
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else {
try {
@ -37,13 +37,8 @@ public class ClipboardManager {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
} catch (NoSuchMethodException e3) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
}
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
}
}
}
@ -53,7 +48,7 @@ public class ClipboardManager {
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else {
try {
@ -76,7 +71,7 @@ public class ClipboardManager {
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
}
@ -85,16 +80,14 @@ public class ClipboardManager {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
case 1:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
case 2:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
}
}
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
return;
}
@ -140,7 +133,7 @@ public class ClipboardManager {
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
return;
}
@ -160,7 +153,7 @@ public class ClipboardManager {
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {

View File

@ -54,7 +54,7 @@ public class ContentProvider implements Closeable {
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (Build.VERSION.SDK_INT >= 31) {
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0;
} else {
@ -83,7 +83,7 @@ public class ContentProvider implements Closeable {
Method method = getCallMethod();
Object[] args;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
if (Build.VERSION.SDK_INT >= 31 && callMethodVersion == 0) {
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
} else {
switch (callMethodVersion) {

View File

@ -90,7 +90,7 @@ public final class SurfaceControl {
if (getBuiltInDisplayMethod == null) {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
} else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
@ -102,7 +102,7 @@ public final class SurfaceControl {
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (Build.VERSION.SDK_INT < 29) {
// call getBuiltInDisplay(0)
return (IBinder) method.invoke(null, 0);
}

View File

@ -4,7 +4,6 @@ import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -109,13 +108,4 @@ public final class WindowManager {
throw new AssertionError(e);
}
}
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
Class<?> cls = manager.getClass();
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,15 @@
package android.content;
public class AttributionSource {
public static class Builder {
public Builder(int uid) {
throw new UnsupportedOperationException();
}
public Builder setPackageName(String value) {
throw new UnsupportedOperationException();
}
public AttributionSource build() {
throw new UnsupportedOperationException();
}
}
}