Compare commits

..

8 Commits

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

View File

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

View File

@@ -183,10 +183,6 @@ It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR. Also see \fB\-\-hid\-keyboard\fR.
.TP
.B \-\-no\-audio
Disable audio forwarding.
.TP .TP
.B \-\-no\-cleanup .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. By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.

View File

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

View File

@@ -1,185 +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");
}
return true;
}
void
sc_display_destroy(struct sc_display *display) {
#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) {
LOGE("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;
}
bool
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
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;
}
bool
sc_display_update_texture(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) {
LOGE("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;
}
bool
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation) {
SDL_RenderClear(display->renderer);
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 false;
}
} 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 false;
}
}
SDL_RenderPresent(display->renderer);
return true;
}

View File

@@ -1,45 +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;
};
bool
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
void
sc_display_destroy(struct sc_display *display);
bool
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
bool
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
bool
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 void
sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
const SDL_Event *event) {
bool control = im->controller; bool control = im->controller;
switch (event->type) { switch (event->type) {
case SDL_TEXTINPUT: 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); const struct sc_input_manager_params *params);
void void
sc_input_manager_handle_event(struct sc_input_manager *im, sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event);
const SDL_Event *event);
#endif #endif

View File

@@ -630,6 +630,17 @@ aoa_hid_end:
} }
controller_started = true; controller_started = true;
controller = &s->controller; 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 // There is a controller if and only if control is enabled
@@ -717,18 +728,6 @@ aoa_hid_end:
audio_demuxer_started = true; 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); ret = event_loop(s);
LOGD("quit..."); LOGD("quit...");

View File

@@ -239,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 // render the texture to the renderer
// //
// Set the update_content_rect flag if the window or content size may have // Set the update_content_rect flag if the window or content size may have
@@ -249,11 +278,35 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
sc_screen_update_content_rect(screen); sc_screen_update_content_rect(screen);
} }
bool ok = sc_display_render(&screen->display, &screen->rect, SDL_RenderClear(screen->renderer);
screen->rotation); if (screen->rotation == 0) {
(void) ok; // error already logged 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__) #if defined(__APPLE__) || defined(__WINDOWS__)
# define CONTINUOUS_RESIZING_WORKAROUND # define CONTINUOUS_RESIZING_WORKAROUND
#endif #endif
@@ -400,11 +453,46 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_fps_counter; goto error_destroy_fps_counter;
} }
ok = sc_display_init(&screen->display, screen->window, params->mipmaps); screen->renderer = SDL_CreateRenderer(screen->window, -1,
if (!ok) { SDL_RENDERER_ACCELERATED);
if (!screen->renderer) {
LOGE("Could not create renderer: %s", SDL_GetError());
goto error_destroy_window; 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(); SDL_Surface *icon = scrcpy_icon_load();
if (icon) { if (icon) {
SDL_SetWindowIcon(screen->window, icon); SDL_SetWindowIcon(screen->window, icon);
@@ -416,7 +504,7 @@ sc_screen_init(struct sc_screen *screen,
screen->frame = av_frame_alloc(); screen->frame = av_frame_alloc();
if (!screen->frame) { if (!screen->frame) {
LOG_OOM(); LOG_OOM();
goto error_destroy_display; goto error_destroy_renderer;
} }
struct sc_input_manager_params im_params = { struct sc_input_manager_params im_params = {
@@ -451,8 +539,8 @@ sc_screen_init(struct sc_screen *screen,
return true; return true;
error_destroy_display: error_destroy_renderer:
sc_display_destroy(&screen->display); SDL_DestroyRenderer(screen->renderer);
error_destroy_window: error_destroy_window:
SDL_DestroyWindow(screen->window); SDL_DestroyWindow(screen->window);
error_destroy_fps_counter: error_destroy_fps_counter:
@@ -508,8 +596,11 @@ sc_screen_destroy(struct sc_screen *screen) {
#ifndef NDEBUG #ifndef NDEBUG
assert(!screen->open); assert(!screen->open);
#endif #endif
sc_display_destroy(&screen->display);
av_frame_free(&screen->frame); av_frame_free(&screen->frame);
if (screen->texture) {
SDL_DestroyTexture(screen->texture);
}
SDL_DestroyRenderer(screen->renderer);
SDL_DestroyWindow(screen->window); SDL_DestroyWindow(screen->window);
sc_fps_counter_destroy(&screen->fps_counter); sc_fps_counter_destroy(&screen->fps_counter);
sc_frame_buffer_destroy(&screen->fb); sc_frame_buffer_destroy(&screen->fb);
@@ -576,6 +667,7 @@ static bool
sc_screen_init_size(struct sc_screen *screen) { sc_screen_init_size(struct sc_screen *screen) {
// Before first frame // Before first frame
assert(!screen->has_frame); assert(!screen->has_frame);
assert(!screen->texture);
// The requested size is passed via screen->frame_size // The requested size is passed via screen->frame_size
@@ -583,27 +675,48 @@ sc_screen_init_size(struct sc_screen *screen) {
get_rotated_size(screen->frame_size, screen->rotation); get_rotated_size(screen->frame_size, screen->rotation);
screen->content_size = content_size; screen->content_size = content_size;
return sc_display_set_texture_size(&screen->display, screen->frame_size); 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 // recreate the texture and resize the window if the frame size has changed
static bool static bool
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width == new_frame_size.width if (screen->frame_size.width != new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) { || screen->frame_size.height != new_frame_size.height) {
return true; // 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 return true;
screen->frame_size = new_frame_size; }
struct sc_size new_content_size = // write the frame into the texture
get_rotated_size(new_frame_size, screen->rotation); static void
set_content_size(screen, new_content_size); 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); if (screen->mipmaps) {
SDL_GL_BindTexture(screen->texture, NULL, NULL);
return sc_display_set_texture_size(&screen->display, screen->frame_size); screen->gl.GenerateMipmap(GL_TEXTURE_2D);
SDL_GL_UnbindTexture(screen->texture);
}
} }
static bool static bool
@@ -618,10 +731,7 @@ sc_screen_update_frame(struct sc_screen *screen) {
if (!prepare_for_frame(screen, new_frame_size)) { if (!prepare_for_frame(screen, new_frame_size)) {
return false; return false;
} }
update_texture(screen, frame);
if (!sc_display_update_texture(&screen->display, frame)) {
return false;
}
if (!screen->has_frame) { if (!screen->has_frame) {
screen->has_frame = true; screen->has_frame = true;
@@ -702,7 +812,7 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
} }
bool 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); bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) { switch (event->type) {

View File

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

View File

@@ -217,18 +217,6 @@ static void test_get_ip_multiline_second_ok(void) {
free(ip); 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) { static void test_get_ip_no_wlan(void) {
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src " char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
"192.168.12.34\r\r\n"; "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_single_line_with_trailing_space();
test_get_ip_multiline_first_ok(); test_get_ip_multiline_first_ok();
test_get_ip_multiline_second_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();
test_get_ip_no_wlan_without_eol(); test_get_ip_no_wlan_without_eol();
test_get_ip_truncated(); test_get_ip_truncated();

View File

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

View File

@@ -5,6 +5,7 @@ import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioRecord; import android.media.AudioRecord;
@@ -14,6 +15,7 @@ import android.media.MediaRecorder;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import java.lang.reflect.Method;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public final class AudioCapture { public final class AudioCapture {
@@ -42,13 +44,28 @@ public final class AudioCapture {
return builder.build(); 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) @TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"}) @SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() { private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder(); 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 // On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get()); setBuilderContext(builder, FakeContext.get());
} }
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat()); builder.setAudioFormat(createAudioFormat());
@@ -86,8 +103,8 @@ public final class AudioCapture {
} catch (UnsupportedOperationException e) { } catch (UnsupportedOperationException e) {
if (attempts == 0) { if (attempts == 0) {
Ln.e("Failed to start audio capture"); 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 " Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
+ "scrcpy."); "scrcpy.");
throw new AudioCaptureForegroundException(); throw new AudioCaptureForegroundException();
} else { } else {
Ln.d("Failed to start audio capture, retrying..."); Ln.d("Failed to start audio capture, retrying...");
@@ -102,7 +119,7 @@ public final class AudioCapture {
} }
public void start() throws AudioCaptureForegroundException { public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT == 30) {
startWorkaroundAndroid11(); startWorkaroundAndroid11();
try { try {
tryStartRecording(3, 100); tryStartRecording(3, 100);
@@ -121,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) { public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, size); int r = recorder.read(directBuffer, size);
if (r <= 0) { if (r <= 0) {
@@ -130,7 +161,7 @@ public final class AudioCapture {
long pts; long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); int ret = getRecorderTimestamp(recorder, timestamp);
if (ret == AudioRecord.SUCCESS) { if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000; pts = timestamp.nanoTime / 1000;
} else { } else {

View File

@@ -84,7 +84,7 @@ public final class AudioEncoder implements AsyncProcessor {
return format; return format;
} }
@TargetApi(Build.VERSION_CODES.N) @TargetApi(24)
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
@@ -159,7 +159,7 @@ public final class AudioEncoder implements AsyncProcessor {
@TargetApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException { 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"); Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false); streamer.writeDisableStream(false);
return; return;
@@ -290,7 +290,7 @@ public final class AudioEncoder implements AsyncProcessor {
} }
private class EncoderCallback extends MediaCodec.Callback { private class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N) @TargetApi(24)
@Override @Override
public void onInputBufferAvailable(MediaCodec codec, int index) { public void onInputBufferAvailable(MediaCodec codec, int index) {
try { try {

View File

@@ -1,7 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.Build;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -20,12 +19,6 @@ public final class AudioRawRecorder implements AsyncProcessor {
} }
private void record() throws IOException, AudioCaptureForegroundException { 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 ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

View File

@@ -373,8 +373,8 @@ public class Controller implements AsyncProcessor {
private void getClipboard(int copyKey) { private void getClipboard(int copyKey) {
// On Android >= 7, press the COPY or CUT key if requested // 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()) { if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; 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 // 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); device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
} }
@@ -397,8 +397,8 @@ public class Controller implements AsyncProcessor {
} }
// On Android >= 7, also press the PASTE key if requested // On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { if (paste && Build.VERSION.SDK_INT >= 24 && device.supportsInputEvents()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); device.pressReleaseKeycode(279, Device.INJECT_MODE_ASYNC);
} }
if (sequence != ControlMessage.SEQUENCE_INVALID) { if (sequence != ControlMessage.SEQUENCE_INVALID) {

View File

@@ -68,7 +68,8 @@ public final class DesktopConnection implements Closeable {
LocalSocket controlSocket = null; LocalSocket controlSocket = null;
try { try {
if (tunnelForward) { if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { LocalServerSocket localServerSocket = new LocalServerSocket(socketName);
try {
videoSocket = localServerSocket.accept(); videoSocket = localServerSocket.accept();
if (sendDummyByte) { if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error // send one byte so the client may read() to detect a connection error
@@ -80,6 +81,8 @@ public final class DesktopConnection implements Closeable {
if (control) { if (control) {
controlSocket = localServerSocket.accept(); controlSocket = localServerSocket.accept();
} }
} finally {
localServerSocket.close();
} }
} else { } else {
videoSocket = connect(socketName); videoSocket = connect(socketName);

View File

@@ -124,7 +124,7 @@ public final class Device {
} }
// main display or any display on Android >= Q // 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) { if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10"); Ln.w("Input events are not supported for secondary displays before Android 10");
} }
@@ -173,7 +173,7 @@ public final class Device {
} }
public static boolean supportsInputEvents(int displayId) { 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() { public boolean supportsInputEvents() {
@@ -277,7 +277,7 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants * @param mode one of the {@code POWER_MODE_*} constants
*/ */
public static boolean setScreenPowerMode(int mode) { 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 // Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) { if (physicalDisplayIds == null) {

View File

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

View File

@@ -3,7 +3,6 @@ package com.genymobile.scrcpy;
import android.graphics.Rect; import android.graphics.Rect;
import java.util.List; import java.util.List;
import java.util.Locale;
public class Options { public class Options {
@@ -47,82 +46,166 @@ public class Options {
return logLevel; return logLevel;
} }
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getScid() { public int getScid() {
return scid; return scid;
} }
public void setScid(int scid) {
this.scid = scid;
}
public boolean getAudio() { public boolean getAudio() {
return audio; return audio;
} }
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public VideoCodec getVideoCodec() { public VideoCodec getVideoCodec() {
return videoCodec; return videoCodec;
} }
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() { public AudioCodec getAudioCodec() {
return audioCodec; return audioCodec;
} }
public void setAudioCodec(AudioCodec audioCodec) {
this.audioCodec = audioCodec;
}
public int getVideoBitRate() { public int getVideoBitRate() {
return videoBitRate; return videoBitRate;
} }
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() { public int getAudioBitRate() {
return audioBitRate; return audioBitRate;
} }
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
}
public int getMaxFps() { public int getMaxFps() {
return maxFps; return maxFps;
} }
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public int getLockVideoOrientation() { public int getLockVideoOrientation() {
return lockVideoOrientation; return lockVideoOrientation;
} }
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() { public Rect getCrop() {
return crop; return crop;
} }
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getControl() { public boolean getControl() {
return control; return control;
} }
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() { public int getDisplayId() {
return displayId; return displayId;
} }
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() { public boolean getShowTouches() {
return showTouches; return showTouches;
} }
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() { public boolean getStayAwake() {
return stayAwake; return stayAwake;
} }
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public List<CodecOption> getVideoCodecOptions() { public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions; return videoCodecOptions;
} }
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
}
public List<CodecOption> getAudioCodecOptions() { public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions; return audioCodecOptions;
} }
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() { public String getVideoEncoder() {
return videoEncoder; return videoEncoder;
} }
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() { public String getAudioEncoder() {
return audioEncoder; return audioEncoder;
} }
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() { public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose; return this.powerOffScreenOnClose;
} }
@@ -131,204 +214,79 @@ public class Options {
return clipboardAutosync; return clipboardAutosync;
} }
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
public boolean getDownsizeOnError() { public boolean getDownsizeOnError() {
return downsizeOnError; return downsizeOnError;
} }
public void setDownsizeOnError(boolean downsizeOnError) {
this.downsizeOnError = downsizeOnError;
}
public boolean getCleanup() { public boolean getCleanup() {
return cleanup; return cleanup;
} }
public void setCleanup(boolean cleanup) {
this.cleanup = cleanup;
}
public boolean getPowerOn() { public boolean getPowerOn() {
return powerOn; return powerOn;
} }
public void setPowerOn(boolean powerOn) {
this.powerOn = powerOn;
}
public boolean getListEncoders() { public boolean getListEncoders() {
return listEncoders; return listEncoders;
} }
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() { public boolean getListDisplays() {
return listDisplays; return listDisplays;
} }
public void setListDisplays(boolean listDisplays) {
this.listDisplays = listDisplays;
}
public boolean getSendDeviceMeta() { public boolean getSendDeviceMeta() {
return sendDeviceMeta; return sendDeviceMeta;
} }
public void setSendDeviceMeta(boolean sendDeviceMeta) {
this.sendDeviceMeta = sendDeviceMeta;
}
public boolean getSendFrameMeta() { public boolean getSendFrameMeta() {
return sendFrameMeta; return sendFrameMeta;
} }
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getSendDummyByte() { public boolean getSendDummyByte() {
return sendDummyByte; return sendDummyByte;
} }
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getSendCodecMeta() { public boolean getSendCodecMeta() {
return sendCodecMeta; return sendCodecMeta;
} }
@SuppressWarnings("MethodLength") public void setSendCodecMeta(boolean sendCodecMeta) {
public static Options parse(String... args) { this.sendCodecMeta = sendCodecMeta;
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 "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);
} }
} }

View File

@@ -252,7 +252,7 @@ public class ScreenEncoder implements Device.RotationListener {
private static IBinder createDisplay() { private static IBinder createDisplay() {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // 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". // 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)); .equals(Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure); return SurfaceControl.createDisplay("scrcpy", secure);
} }

View File

@@ -1,11 +1,13 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
public final class Server { public final class Server {
@@ -86,7 +88,7 @@ public final class Server {
// Before Android 11, audio is not supported. // Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord. // 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. // 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(); Workarounds.fillAppContext();
} }
@@ -159,12 +161,203 @@ public final class Server {
return thread; 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 { public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
}); });
Options options = Options.parse(args); Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel()); Ln.initLogLevel(options.getLogLevel());

View File

@@ -34,7 +34,7 @@ public final class Settings {
} }
public static String getValue(String table, String key) throws SettingsException { 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> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key); 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 { 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> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value); 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 { 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> // on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key); 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), H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC), H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
@SuppressLint("InlinedApi") // introduced in API 21 @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 int id; // 4-byte ASCII representation of the name
private final String name; private final String name;

View File

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

View File

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

View File

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

View File

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

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