Compare commits

..

13 Commits

Author SHA1 Message Date
Romain Vimont
1038bad385 Make it work over tcpip
"adb reverse" currently does not work over tcpip (i.e. on a device
connected by "adb connect"):
<https://issuetracker.google.com/issues/37066218>

To work around the problem, if the call to "adb reverse" fails, then
fallback to "adb forward", and reverse the client/server roles.

Keep the "adb reverse" mode as the default because it does not involve
connection retries: when using "adb forward", the client must try to
connect successively until the server listens.

Due to the tunnel, every connect() will succeed, so the client must
attempt to read() to detect a connection failure. For this purpose, when
using the "adb forward" mode, the server initially writes a dummy byte,
read by the client.

Fixes <https://github.com/Genymobile/scrcpy/issues/5>.
2018-03-12 14:10:32 +01:00
Romain Vimont
2b3ed5bcdb Store serial in server instance
The serial is needed for many server actions, but this is an
implementation detail, so the caller should not have to provide it on
every call.

Instead, store the serial in the server instance on server_start().

This paves the way to implement the "adb forward" fallback properly.
2018-03-12 14:10:32 +01:00
Romain Vimont
9e328ef98b Always use the best render scale quality available
Because why not.

See <https://wiki.libsdl.org/SDL_HINT_RENDER_SCALE_QUALITY>.
2018-03-12 14:10:12 +01:00
Romain Vimont
c075ad0a1e Fix mouse clicks on LG devices
Use default values (0) for some fields of PointerCoords so that mouse
clicks work correctly on LG devices.

Fixes <https://github.com/Genymobile/scrcpy/issues/18>.
2018-03-11 14:11:43 +01:00
Romain Vimont
dac7196bd6 Support screens with dimensions not divisible by 8
The codec only supports dimensions which are multiple of 8.

Thus, when --max-size is specified, the value is always rounded down to
the nearest multiple of 8.

However, it was wrongly assumed that the physical size is always a
multiple of 8. To support such devices, also round down the physical
screen dimensions.

Fixes <https://github.com/Genymobile/scrcpy/issues/39>.
2018-03-11 14:09:25 +01:00
Romain Vimont
c87d94ee27 Map middle-click to HOME
Middle-click is useless in practice. Use it for HOME.
2018-03-10 00:44:19 +01:00
Romain Vimont
675704c71c Map right-click to BACK if screen is on
Right-click was used to turn the screen on. It did nothing when the
screen was already on.

Instead, in that case, press BACK (like Vysor).

Suggested by: <https://www.reddit.com/r/Android/comments/834zmr/introducing_scrcpy_an_app_to_display_and_control/dvfueft/>
2018-03-10 00:16:29 +01:00
Romain Vimont
9396ea6d42 Fix text input event segfault
The text input control_event was initially designed for mapping
SDL_TextInputEvent, limited to 32 characters.

For simplicity, the copy/paste feature was implemented using the same
control_event: it just sends the text to paste.

However, the pasted text might have a length breaking some assumptions:
 - on the client, the event max-size was smaller than the text
   max-length,
 - on the server, the raw buffer storing the events was smaller than the
   max event size.

Fix these inconsistencies, and encode the length on 2 bytes, to accept
more than 256 characters.

Fixes <https://github.com/Genymobile/scrcpy/issues/10>.
2018-03-09 22:30:10 +01:00
Romain Vimont
f9562f537a Unref the packet on error
Do not leak the packet data on error.
2018-03-08 21:36:04 +01:00
Romain Vimont
a34fbd23e9 Do not leak the packet data
Oops! The content of the packets were never freed.
2018-03-08 20:46:02 +01:00
Romain Vimont
e8b8a570e7 Document ./run script usage
Indicate how to run the app from the build directory in README. It's
convenient during development.
2018-03-08 14:53:18 +01:00
Romain Vimont
0e9a76c0c4 Add link to blog article in README 2018-03-08 12:31:26 +01:00
Romain Vimont
f9f305d19d Update release checksums in README 2018-03-08 11:34:54 +01:00
23 changed files with 308 additions and 93 deletions

View File

@@ -54,7 +54,7 @@ For Windows, for simplicity, a prebuilt archive with all the dependencies
(including `adb`) is available: (including `adb`) is available:
- [`scrcpy-windows-with-deps-v1.0.zip`][direct-windows-with-deps]. - [`scrcpy-windows-with-deps-v1.0.zip`][direct-windows-with-deps].
_(SHA-256: TODO)_ _(SHA-256: bc4bf32600e8548cdce490f94bed5dcba0006cdd38aff95748972e5d9877dd62)_
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-windows-with-deps-v1.0.zip [direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-windows-with-deps-v1.0.zip
@@ -138,7 +138,7 @@ depend on your system and architecture, you may want to use the prebuilt binary
instead: instead:
- [`scrcpy-server-v1.0.jar`][direct-scrcpy-server]. - [`scrcpy-server-v1.0.jar`][direct-scrcpy-server].
_(SHA-256: TODO)_ _(SHA-256: b573b06a6072476b85b6308e3ad189f2665ad5be4f8ca3a6b9ec81d64df20558)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-server-v1.0.jar [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.0/scrcpy-server-v1.0.jar
@@ -179,23 +179,31 @@ If several devices are listed in `adb devices`, you must specify the _serial_:
scrcpy -s 0123456789abcdef scrcpy -s 0123456789abcdef
To run without installing:
./run x [options]
(where `x` is your build directory).
## Shortcuts ## Shortcuts
| Action | Shortcut | | Action | Shortcut |
| ------------------------------------- | -------------:| | -------------------------------------- |:---------------------------- |
| switch fullscreen mode | `Ctrl`+`f` | | switch fullscreen mode | `Ctrl`+`f` |
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` | | resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
| resize window to remove black borders | `Ctrl`+`x` | | resize window to remove black borders | `Ctrl`+`x` |
| click on `HOME` | `Ctrl`+`h` | | click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
| click on `BACK` | `Ctrl`+`b` | | click on `BACK` | `Ctrl`+`b` \| _Right-click¹_ |
| click on `APP_SWITCH` | `Ctrl`+`m` | | click on `APP_SWITCH` | `Ctrl`+`m` |
| click on `VOLUME_UP` | `Ctrl`+`+` | | click on `VOLUME_UP` | `Ctrl`+`+` |
| click on `VOLUME_DOWN` | `Ctrl`+`-` | | click on `VOLUME_DOWN` | `Ctrl`+`-` |
| click on `POWER` | `Ctrl`+`p` | | click on `POWER` | `Ctrl`+`p` |
| turn screen on | _Right-click_ | | turn screen on | _Right-click¹_ |
| paste computer clipboard to device | `Ctrl`+`v` | | paste computer clipboard to device | `Ctrl`+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` | | enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Right-click turns the screen on if it was off, presses BACK otherwise._
## Why _scrcpy_? ## Why _scrcpy_?
@@ -230,3 +238,7 @@ Read the [developers page].
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
## Article
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)

View File

@@ -45,6 +45,13 @@ process_t adb_forward(const char *serial, uint16_t local_port, const char *devic
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
} }
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port);
const char *const adb_cmd[] = {"forward", "--remove", local};
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
}
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) { process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
char local[4 + 5 + 1]; // tcp:PORT char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME

View File

@@ -39,6 +39,7 @@ SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len); process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name); process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
process_t adb_forward_remove(const char *serial, uint16_t local_port);
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port); process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
process_t adb_reverse_remove(const char *serial, const char *device_socket_name); process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
process_t adb_push(const char *serial, const char *local, const char *remote); process_t adb_push(const char *serial, const char *local, const char *remote);

View File

@@ -39,9 +39,9 @@ int control_event_serialize(const struct control_event *event, unsigned char *bu
// injecting a text takes time, so limit the text length // injecting a text takes time, so limit the text length
len = TEXT_MAX_LENGTH; len = TEXT_MAX_LENGTH;
} }
buf[1] = (Uint8) len; write16(&buf[1], (Uint16) len);
memcpy(&buf[2], event->text_event.text, len); memcpy(&buf[3], event->text_event.text, len);
return 2 + len; return 3 + len;
} }
case CONTROL_EVENT_TYPE_MOUSE: case CONTROL_EVENT_TYPE_MOUSE:
buf[1] = event->mouse_event.action; buf[1] = event->mouse_event.action;

View File

@@ -9,8 +9,8 @@
#include "common.h" #include "common.h"
#define CONTROL_EVENT_QUEUE_SIZE 64 #define CONTROL_EVENT_QUEUE_SIZE 64
#define SERIALIZED_EVENT_MAX_SIZE 33 #define TEXT_MAX_LENGTH 300
#define TEXT_MAX_LENGTH 256 #define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
enum control_event_type { enum control_event_type {
CONTROL_EVENT_TYPE_KEYCODE, CONTROL_EVENT_TYPE_KEYCODE,
@@ -20,7 +20,7 @@ enum control_event_type {
CONTROL_EVENT_TYPE_COMMAND, CONTROL_EVENT_TYPE_COMMAND,
}; };
#define CONTROL_EVENT_COMMAND_SCREEN_ON 0 #define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
struct control_event { struct control_event {
enum control_event_type type; enum control_event_type type;

View File

@@ -106,6 +106,7 @@ static int run_decoder(void *data) {
push_frame(decoder); push_frame(decoder);
} else if (ret != AVERROR(EAGAIN)) { } else if (ret != AVERROR(EAGAIN)) {
LOGE("Could not receive video frame: %d", ret); LOGE("Could not receive video frame: %d", ret);
av_packet_unref(&packet);
goto run_quit; goto run_quit;
} }
#else #else
@@ -123,6 +124,7 @@ static int run_decoder(void *data) {
packet.data += len; packet.data += len;
} }
#endif #endif
av_packet_unref(&packet);
} }
LOGD("End of frames"); LOGD("End of frames");

View File

@@ -78,10 +78,11 @@ static inline void action_volume_down(struct controller *controller) {
send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN"); send_keycode(controller, AKEYCODE_VOLUME_DOWN, "VOLUME_DOWN");
} }
static void turn_screen_on(struct controller *controller) { // turn the screen on if it was off, press BACK otherwise
static void press_back_or_turn_screen_on(struct controller *controller) {
struct control_event control_event; struct control_event control_event;
control_event.type = CONTROL_EVENT_TYPE_COMMAND; control_event.type = CONTROL_EVENT_TYPE_COMMAND;
control_event.command_event.action = CONTROL_EVENT_COMMAND_SCREEN_ON; control_event.command_event.action = CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON;
if (!controller_push_event(controller, &control_event)) { if (!controller_push_event(controller, &control_event)) {
LOGW("Cannot turn screen on"); LOGW("Cannot turn screen on");
@@ -225,9 +226,15 @@ void input_manager_process_mouse_motion(struct input_manager *input_manager,
void input_manager_process_mouse_button(struct input_manager *input_manager, void input_manager_process_mouse_button(struct input_manager *input_manager,
const SDL_MouseButtonEvent *event) { const SDL_MouseButtonEvent *event) {
if (event->button == SDL_BUTTON_RIGHT && event->type == SDL_MOUSEBUTTONDOWN) { if (event->type == SDL_MOUSEBUTTONDOWN) {
turn_screen_on(input_manager->controller); if (event->button == SDL_BUTTON_RIGHT) {
return; press_back_or_turn_screen_on(input_manager->controller);
return;
}
if (event->button == SDL_BUTTON_MIDDLE) {
action_home(input_manager->controller);
return;
}
}; };
struct control_event control_event; struct control_event control_event;
if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) { if (mouse_button_from_sdl_to_android(event, input_manager->screen->frame_size, &control_event)) {

View File

@@ -61,10 +61,12 @@ static void usage(const char *arg0) {
"\n" "\n"
" Ctrl+h\n" " Ctrl+h\n"
" Home\n" " Home\n"
" Middle-click\n"
" click on HOME\n" " click on HOME\n"
"\n" "\n"
" Ctrl+b\n" " Ctrl+b\n"
" Ctrl+Backspace\n" " Ctrl+Backspace\n"
" Right-click (when screen is on)\n"
" click on BACK\n" " click on BACK\n"
"\n" "\n"
" Ctrl+m\n" " Ctrl+m\n"
@@ -79,7 +81,7 @@ static void usage(const char *arg0) {
" Ctrl+p\n" " Ctrl+p\n"
" click on POWER (turn screen on/off)\n" " click on POWER (turn screen on/off)\n"
"\n" "\n"
" Right-click\n" " Right-click (when screen is off)\n"
" turn screen on\n" " turn screen on\n"
"\n" "\n"
" Ctrl+v\n" " Ctrl+v\n"

View File

@@ -18,6 +18,26 @@
typedef struct in_addr IN_ADDR; typedef struct in_addr IN_ADDR;
#endif #endif
socket_t net_connect(Uint32 addr, Uint16 port) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
perror("socket");
return INVALID_SOCKET;
}
SOCKADDR_IN sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(addr);
sin.sin_port = htons(port);
if (connect(sock, (SOCKADDR *) &sin, sizeof(sin)) == SOCKET_ERROR) {
perror("connect");
return INVALID_SOCKET;
}
return sock;
}
socket_t net_listen(Uint32 addr, Uint16 port, int backlog) { socket_t net_listen(Uint32 addr, Uint16 port, int backlog) {
socket_t sock = socket(AF_INET, SOCK_STREAM, 0); socket_t sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) { if (sock == INVALID_SOCKET) {

View File

@@ -21,6 +21,7 @@
SDL_bool net_init(void); SDL_bool net_init(void);
void net_cleanup(void); void net_cleanup(void);
socket_t net_connect(Uint32 addr, Uint16 port);
socket_t net_listen(Uint32 addr, Uint16 port, int backlog); socket_t net_listen(Uint32 addr, Uint16 port, int backlog);
socket_t net_accept(socket_t server_socket); socket_t net_accept(socket_t server_socket);

View File

@@ -103,9 +103,9 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// managed by the event loop. This blocking call blocks the event loop, so // managed by the event loop. This blocking call blocks the event loop, so
// timeout the connection not to block indefinitely in case of SIGTERM. // timeout the connection not to block indefinitely in case of SIGTERM.
#define SERVER_CONNECT_TIMEOUT_MS 2000 #define SERVER_CONNECT_TIMEOUT_MS 2000
socket_t device_socket = server_connect_to(&server, serial, SERVER_CONNECT_TIMEOUT_MS); socket_t device_socket = server_connect_to(&server, SERVER_CONNECT_TIMEOUT_MS);
if (device_socket == INVALID_SOCKET) { if (device_socket == INVALID_SOCKET) {
server_stop(&server, serial); server_stop(&server);
ret = SDL_FALSE; ret = SDL_FALSE;
goto finally_destroy_server; goto finally_destroy_server;
} }
@@ -117,13 +117,13 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// therefore, we transmit the screen size before the video stream, to be able // therefore, we transmit the screen size before the video stream, to be able
// to init the window immediately // to init the window immediately
if (!device_read_info(device_socket, device_name, &frame_size)) { if (!device_read_info(device_socket, device_name, &frame_size)) {
server_stop(&server, serial); server_stop(&server);
ret = SDL_FALSE; ret = SDL_FALSE;
goto finally_destroy_server; goto finally_destroy_server;
} }
if (!frames_init(&frames)) { if (!frames_init(&frames)) {
server_stop(&server, serial); server_stop(&server);
ret = SDL_FALSE; ret = SDL_FALSE;
goto finally_destroy_server; goto finally_destroy_server;
} }
@@ -134,7 +134,7 @@ SDL_bool scrcpy(const char *serial, Uint16 local_port, Uint16 max_size, Uint32 b
// start the decoder // start the decoder
if (!decoder_start(&decoder)) { if (!decoder_start(&decoder)) {
ret = SDL_FALSE; ret = SDL_FALSE;
server_stop(&server, serial); server_stop(&server);
goto finally_destroy_frames; goto finally_destroy_frames;
} }
@@ -165,7 +165,7 @@ finally_destroy_controller:
finally_stop_decoder: finally_stop_decoder:
decoder_stop(&decoder); decoder_stop(&decoder);
// stop the server before decoder_join() to wake up the decoder // stop the server before decoder_join() to wake up the decoder
server_stop(&server, serial); server_stop(&server);
decoder_join(&decoder); decoder_join(&decoder);
finally_destroy_frames: finally_destroy_frames:
frames_destroy(&frames); frames_destroy(&frames);

View File

@@ -18,8 +18,8 @@ SDL_bool sdl_init_and_configure(void) {
atexit(SDL_Quit); atexit(SDL_Quit);
// Bilinear resizing // Use the best available scale quality
if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2")) {
LOGW("Could not enable bilinear filtering"); LOGW("Could not enable bilinear filtering");
} }

View File

@@ -4,6 +4,7 @@
#include <inttypes.h> #include <inttypes.h>
#include <stdint.h> #include <stdint.h>
#include <SDL2/SDL_assert.h> #include <SDL2/SDL_assert.h>
#include <SDL2/SDL_timer.h>
#include "config.h" #include "config.h"
#include "log.h" #include "log.h"
@@ -37,17 +38,45 @@ static SDL_bool remove_server(const char *serial) {
return process_check_success(process, "adb shell rm"); return process_check_success(process, "adb shell rm");
} }
static SDL_bool enable_tunnel(const char *serial, Uint16 local_port) { static SDL_bool enable_tunnel_reverse(const char *serial, Uint16 local_port) {
process_t process = adb_reverse(serial, SOCKET_NAME, local_port); process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
return process_check_success(process, "adb reverse"); return process_check_success(process, "adb reverse");
} }
static SDL_bool disable_tunnel(const char *serial) { static SDL_bool disable_tunnel_reverse(const char *serial) {
process_t process = adb_reverse_remove(serial, SOCKET_NAME); process_t process = adb_reverse_remove(serial, SOCKET_NAME);
return process_check_success(process, "adb reverse --remove"); return process_check_success(process, "adb reverse --remove");
} }
static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_rate) { static SDL_bool enable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward(serial, local_port, SOCKET_NAME);
return process_check_success(process, "adb forward");
}
static SDL_bool disable_tunnel_forward(const char *serial, Uint16 local_port) {
process_t process = adb_forward_remove(serial, local_port);
return process_check_success(process, "adb forward --remove");
}
static SDL_bool enable_tunnel(struct server *server) {
if (enable_tunnel_reverse(server->serial, server->local_port)) {
return SDL_TRUE;
}
LOGW("'adb reverse' failed, fallback to 'adb forward'");
server->tunnel_forward = SDL_TRUE;
return enable_tunnel_forward(server->serial, server->local_port);
}
static SDL_bool disable_tunnel(struct server *server) {
if (server->tunnel_forward) {
return disable_tunnel_forward(server->serial, server->local_port);
}
return disable_tunnel_reverse(server->serial);
}
static process_t execute_server(const char *serial,
Uint16 max_size, Uint32 bit_rate, SDL_bool tunnel_forward) {
char max_size_string[6]; char max_size_string[6];
char bit_rate_string[11]; char bit_rate_string[11];
sprintf(max_size_string, "%"PRIu16, max_size); sprintf(max_size_string, "%"PRIu16, max_size);
@@ -60,15 +89,48 @@ static process_t execute_server(const char *serial, Uint16 max_size, Uint32 bit_
"com.genymobile.scrcpy.Server", "com.genymobile.scrcpy.Server",
max_size_string, max_size_string,
bit_rate_string, bit_rate_string,
tunnel_forward ? "true" : "false",
}; };
return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
} }
static socket_t listen_on_port(Uint16 port) {
#define IPV4_LOCALHOST 0x7F000001 #define IPV4_LOCALHOST 0x7F000001
static socket_t listen_on_port(Uint16 port) {
return net_listen(IPV4_LOCALHOST, port, 1); return net_listen(IPV4_LOCALHOST, port, 1);
} }
static socket_t connect_and_read_byte(Uint16 port) {
socket_t socket = net_connect(IPV4_LOCALHOST, port);
if (socket == INVALID_SOCKET) {
return INVALID_SOCKET;
}
char byte;
// the connection may succeed even if the server behind the "adb tunnel"
// is not listening, so read one byte to detect a working connection
if (net_recv_all(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
return INVALID_SOCKET;
}
return socket;
}
static socket_t connect_to_server(Uint16 port, Uint32 attempts, Uint32 delay) {
do {
LOGD("Remaining connection attempts: %d", (int) attempts);
socket_t socket = connect_and_read_byte(port);
if (socket != INVALID_SOCKET) {
// it worked!
return socket;
}
if (attempts) {
SDL_Delay(delay);
}
} while (--attempts > 0);
return INVALID_SOCKET;
}
static void close_socket(socket_t *socket) { static void close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET); SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR); net_shutdown(*socket, SHUT_RDWR);
@@ -85,63 +147,84 @@ void server_init(struct server *server) {
SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port, SDL_bool server_start(struct server *server, const char *serial, Uint16 local_port,
Uint16 max_size, Uint32 bit_rate) { Uint16 max_size, Uint32 bit_rate) {
server->local_port = local_port;
if (serial) {
server->serial = SDL_strdup(serial);
}
if (!push_server(serial)) { if (!push_server(serial)) {
return SDL_FALSE; return SDL_FALSE;
} }
server->server_copied_to_device = SDL_TRUE; server->server_copied_to_device = SDL_TRUE;
if (!enable_tunnel(serial, local_port)) { if (!enable_tunnel(server)) {
return SDL_FALSE; return SDL_FALSE;
} }
// At the application level, the device part is "the server" because it // if "adb reverse" does not work (e.g. over "adb connect"), it fallbacks to
// serves video stream and control. However, at the network level, the // "adb forward", so the app socket is the client
// client listens and the server connects to the client. That way, the if (!server->tunnel_forward) {
// client can listen before starting the server app, so there is no need to // At the application level, the device part is "the server" because it
// try to connect until the server socket is listening on the device. // serves video stream and control. However, at the network level, the
// client listens and the server connects to the client. That way, the
// client can listen before starting the server app, so there is no need to
// try to connect until the server socket is listening on the device.
server->server_socket = listen_on_port(local_port); server->server_socket = listen_on_port(local_port);
if (server->server_socket == INVALID_SOCKET) { if (server->server_socket == INVALID_SOCKET) {
LOGE("Could not listen on port %" PRIu16, local_port); LOGE("Could not listen on port %" PRIu16, local_port);
disable_tunnel(serial); disable_tunnel(server);
return SDL_FALSE; return SDL_FALSE;
}
} }
// server will connect to our server socket // server will connect to our server socket
server->process = execute_server(serial, max_size, bit_rate); server->process = execute_server(serial, max_size, bit_rate, server->tunnel_forward);
if (server->process == PROCESS_NONE) { if (server->process == PROCESS_NONE) {
close_socket(&server->server_socket); if (!server->tunnel_forward) {
disable_tunnel(serial); close_socket(&server->server_socket);
}
disable_tunnel(server);
return SDL_FALSE; return SDL_FALSE;
} }
server->adb_reverse_enabled = SDL_TRUE; server->tunnel_enabled = SDL_TRUE;
return SDL_TRUE; return SDL_TRUE;
} }
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms) { socket_t server_connect_to(struct server *server, Uint32 timeout_ms) {
server->device_socket = net_accept(server->server_socket); if (!server->tunnel_forward) {
server->device_socket = net_accept(server->server_socket);
} else {
Uint32 attempts = 10;
Uint32 delay = 100; // ms
server->device_socket = connect_to_server(server->local_port, attempts, delay);
}
if (server->device_socket == INVALID_SOCKET) { if (server->device_socket == INVALID_SOCKET) {
return INVALID_SOCKET; return INVALID_SOCKET;
} }
// we don't need the server socket anymore if (!server->tunnel_forward) {
close_socket(&server->server_socket); // we don't need the server socket anymore
close_socket(&server->server_socket);
}
// the server is started, we can clean up the jar from the temporary folder // the server is started, we can clean up the jar from the temporary folder
remove_server(serial); // ignore failure remove_server(server->serial); // ignore failure
server->server_copied_to_device = SDL_FALSE; server->server_copied_to_device = SDL_FALSE;
// we don't need the adb tunnel anymore // we don't need the adb tunnel anymore
disable_tunnel(serial); // ignore failure disable_tunnel(server); // ignore failure
server->adb_reverse_enabled = SDL_FALSE; server->tunnel_enabled = SDL_FALSE;
return server->device_socket; return server->device_socket;
} }
void server_stop(struct server *server, const char *serial) { void server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE); SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) { if (!cmd_terminate(server->process)) {
@@ -151,13 +234,13 @@ void server_stop(struct server *server, const char *serial) {
cmd_simple_wait(server->process, NULL); // ignore exit code cmd_simple_wait(server->process, NULL); // ignore exit code
LOGD("Server terminated"); LOGD("Server terminated");
if (server->adb_reverse_enabled) { if (server->tunnel_enabled) {
// ignore failure // ignore failure
disable_tunnel(serial); disable_tunnel(server);
} }
if (server->server_copied_to_device) { if (server->server_copied_to_device) {
remove_server(serial); // ignore failure remove_server(server->serial); // ignore failure
} }
} }
@@ -168,4 +251,5 @@ void server_destroy(struct server *server) {
if (server->device_socket != INVALID_SOCKET) { if (server->device_socket != INVALID_SOCKET) {
close_socket(&server->device_socket); close_socket(&server->device_socket);
} }
SDL_free((void *) server->serial);
} }

View File

@@ -5,18 +5,24 @@
#include "net.h" #include "net.h"
struct server { struct server {
const char *serial;
process_t process; process_t process;
socket_t server_socket; socket_t server_socket; // only used if !tunnel_forward
socket_t device_socket; socket_t device_socket;
SDL_bool adb_reverse_enabled; Uint16 local_port;
SDL_bool tunnel_enabled;
SDL_bool tunnel_forward; // use "adb forward" instead of "adb reverse"
SDL_bool server_copied_to_device; SDL_bool server_copied_to_device;
}; };
#define SERVER_INITIALIZER { \ #define SERVER_INITIALIZER { \
.serial = NULL, \
.process = PROCESS_NONE, \ .process = PROCESS_NONE, \
.server_socket = INVALID_SOCKET, \ .server_socket = INVALID_SOCKET, \
.device_socket = INVALID_SOCKET, \ .device_socket = INVALID_SOCKET, \
.adb_reverse_enabled = SDL_FALSE, \ .local_port = 0, \
.tunnel_enabled = SDL_FALSE, \
.tunnel_forward = SDL_FALSE, \
.server_copied_to_device = SDL_FALSE, \ .server_copied_to_device = SDL_FALSE, \
} }
@@ -28,10 +34,10 @@ SDL_bool server_start(struct server *server, const char *serial, Uint16 local_po
Uint16 max_size, Uint32 bit_rate); Uint16 max_size, Uint32 bit_rate);
// block until the communication with the server is established // block until the communication with the server is established
socket_t server_connect_to(struct server *server, const char *serial, Uint32 timeout_ms); socket_t server_connect_to(struct server *server, Uint32 timeout_ms);
// disconnect and kill the server process // disconnect and kill the server process
void server_stop(struct server *server, const char *serial); void server_stop(struct server *server);
// close and release sockets // close and release sockets
void server_destroy(struct server *server); void server_destroy(struct server *server);

View File

@@ -35,16 +35,36 @@ static void test_serialize_text_event() {
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE]; unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf); int size = control_event_serialize(&event, buf);
assert(size == 15); assert(size == 16);
const unsigned char expected[] = { const unsigned char expected[] = {
0x01, // CONTROL_EVENT_TYPE_KEYCODE 0x01, // CONTROL_EVENT_TYPE_KEYCODE
0x0d, // text length 0x00, 0x0d, // text length
'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', // text
}; };
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_long_text_event() {
struct control_event event;
event.type = CONTROL_EVENT_TYPE_TEXT;
char text[TEXT_MAX_LENGTH];
memset(text, 'a', sizeof(text));
event.text_event.text = text;
unsigned char buf[SERIALIZED_EVENT_MAX_SIZE];
int size = control_event_serialize(&event, buf);
assert(size == 3 + sizeof(text));
unsigned char expected[3 + TEXT_MAX_LENGTH];
expected[0] = 0x01; // CONTROL_EVENT_TYPE_KEYCODE
expected[1] = 0x01;
expected[2] = 0x2c; // text length (16 bits)
memset(&expected[3], 'a', TEXT_MAX_LENGTH);
assert(!memcmp(buf, expected, sizeof(expected)));
}
static void test_serialize_mouse_event() { static void test_serialize_mouse_event() {
struct control_event event = { struct control_event event = {
.type = CONTROL_EVENT_TYPE_MOUSE, .type = CONTROL_EVENT_TYPE_MOUSE,
@@ -114,6 +134,7 @@ static void test_serialize_scroll_event() {
int main() { int main() {
test_serialize_keycode_event(); test_serialize_keycode_event();
test_serialize_text_event(); test_serialize_text_event();
test_serialize_long_text_event();
test_serialize_mouse_event(); test_serialize_mouse_event();
test_serialize_scroll_event(); test_serialize_scroll_event();
return 0; return 0;

View File

@@ -11,7 +11,7 @@ public final class ControlEvent {
public static final int TYPE_SCROLL = 3; public static final int TYPE_SCROLL = 3;
public static final int TYPE_COMMAND = 4; public static final int TYPE_COMMAND = 4;
public static final int COMMAND_SCREEN_ON = 0; public static final int COMMAND_BACK_OR_SCREEN_ON = 0;
private int type; private int type;
private String text; private String text;

View File

@@ -13,8 +13,8 @@ public class ControlEventReader {
private static final int SCROLL_PAYLOAD_LENGTH = 16; private static final int SCROLL_PAYLOAD_LENGTH = 16;
private static final int COMMAND_PAYLOAD_LENGTH = 1; private static final int COMMAND_PAYLOAD_LENGTH = 1;
private static final int TEXT_MAX_LENGTH = 256; public static final int TEXT_MAX_LENGTH = 300;
private static final int RAW_BUFFER_SIZE = 128; private static final int RAW_BUFFER_SIZE = 1024;
private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE]; private final byte[] rawBuffer = new byte[RAW_BUFFER_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer); private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
@@ -94,7 +94,7 @@ public class ControlEventReader {
if (buffer.remaining() < 1) { if (buffer.remaining() < 1) {
return null; return null;
} }
int len = toUnsigned(buffer.get()); int len = toUnsigned(buffer.getShort());
if (buffer.remaining() < len) { if (buffer.remaining() < len) {
return null; return null;
} }

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.net.LocalServerSocket;
import android.net.LocalSocket; import android.net.LocalSocket;
import android.net.LocalSocketAddress; import android.net.LocalSocketAddress;
@@ -33,8 +34,20 @@ public final class DesktopConnection implements Closeable {
return localSocket; return localSocket;
} }
public static DesktopConnection open(Device device) throws IOException { private static LocalSocket listenAndAccept(String abstractName) throws IOException {
LocalSocket socket = connect(SOCKET_NAME); LocalServerSocket localServerSocket = new LocalServerSocket(abstractName);
return localServerSocket.accept();
}
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket socket;
if (tunnelForward) {
socket = listenAndAccept(SOCKET_NAME);
// send one byte so the client may read() to detect a connection error
socket.getOutputStream().write(0);
} else {
socket = connect(SOCKET_NAME);
}
DesktopConnection connection = new DesktopConnection(socket); DesktopConnection connection = new DesktopConnection(socket);
Size videoSize = device.getScreenInfo().getVideoSize(); Size videoSize = device.getScreenInfo().getVideoSize();

View File

@@ -50,8 +50,8 @@ public final class Device {
DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo(); DisplayInfo displayInfo = serviceManager.getDisplayManager().getDisplayInfo();
boolean rotated = (displayInfo.getRotation() & 1) != 0; boolean rotated = (displayInfo.getRotation() & 1) != 0;
Size deviceSize = displayInfo.getSize(); Size deviceSize = displayInfo.getSize();
int w = deviceSize.getWidth(); int w = deviceSize.getWidth() & ~7; // in case it's not a multiple of 8
int h = deviceSize.getHeight(); int h = deviceSize.getHeight() & ~7;
if (maxSize > 0) { if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) { if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8"); throw new AssertionError("Max size must be a multiple of 8");

View File

@@ -39,10 +39,6 @@ public class EventController {
coords.orientation = 0; coords.orientation = 0;
coords.pressure = 1; coords.pressure = 1;
coords.size = 1; coords.size = 1;
coords.toolMajor = 1;
coords.toolMinor = 1;
coords.touchMajor = 1;
coords.touchMinor = 1;
} }
private void setPointerCoords(Point point) { private void setPointerCoords(Point point) {
@@ -167,10 +163,15 @@ public class EventController {
return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER); return device.isScreenOn() || injectKeycode(KeyEvent.KEYCODE_POWER);
} }
private boolean pressBackOrTurnScreenOn() {
int keycode = device.isScreenOn() ? KeyEvent.KEYCODE_BACK : KeyEvent.KEYCODE_POWER;
return injectKeycode(keycode);
}
private boolean executeCommand(int action) { private boolean executeCommand(int action) {
switch (action) { switch (action) {
case ControlEvent.COMMAND_SCREEN_ON: case ControlEvent.COMMAND_BACK_OR_SCREEN_ON:
return turnScreenOn(); return pressBackOrTurnScreenOn();
default: default:
Ln.w("Unsupported command: " + action); Ln.w("Unsupported command: " + action);
} }

View File

@@ -3,6 +3,7 @@ package com.genymobile.scrcpy;
public class Options { public class Options {
private int maxSize; private int maxSize;
private int bitRate; private int bitRate;
private boolean tunnelForward;
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
@@ -19,4 +20,12 @@ public class Options {
public void setBitRate(int bitRate) { public void setBitRate(int bitRate) {
this.bitRate = bitRate; this.bitRate = bitRate;
} }
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
} }

View File

@@ -10,7 +10,8 @@ public final class Server {
private static void scrcpy(Options options) throws IOException { private static void scrcpy(Options options) throws IOException {
final Device device = new Device(options); final Device device = new Device(options);
try (DesktopConnection connection = DesktopConnection.open(device)) { boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate()); ScreenEncoder screenEncoder = new ScreenEncoder(options.getBitRate());
// asynchronous // asynchronous
@@ -55,6 +56,13 @@ public final class Server {
int bitRate = Integer.parseInt(args[1]); int bitRate = Integer.parseInt(args[1]);
options.setBitRate(bitRate); options.setBitRate(bitRate);
if (args.length < 3) {
return options;
}
// use "adb forward" instead of "adb tunnel"? (so the server must listen)
boolean tunnelForward = Boolean.parseBoolean(args[2]);
options.setTunnelForward(tunnelForward);
return options; return options;
} }

View File

@@ -3,14 +3,15 @@ package com.genymobile.scrcpy;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
public class ControlEventReaderTest { public class ControlEventReaderTest {
@@ -43,8 +44,8 @@ public class ControlEventReaderTest {
DataOutputStream dos = new DataOutputStream(bos); DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT); dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8); byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeByte(text.length); dos.writeShort(text.length);
dos.write("testé".getBytes(StandardCharsets.UTF_8)); dos.write(text);
byte[] packet = bos.toByteArray(); byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet)); reader.readFrom(new ByteArrayInputStream(packet));
@@ -54,6 +55,26 @@ public class ControlEventReaderTest {
Assert.assertEquals("testé", event.getText()); Assert.assertEquals("testé", event.getText());
} }
@Test
public void testParseLongTextEvent() throws IOException {
ControlEventReader reader = new ControlEventReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlEvent.TYPE_TEXT);
byte[] text = new byte[ControlEventReader.TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeShort(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlEvent event = reader.next();
Assert.assertEquals(ControlEvent.TYPE_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
}
@Test @Test
public void testParseMouseEvent() throws IOException { public void testParseMouseEvent() throws IOException {
ControlEventReader reader = new ControlEventReader(); ControlEventReader reader = new ControlEventReader();