From 54d9148a36decdf0dcc412560ae50870a7790fde Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 12 Dec 2017 15:12:07 +0100 Subject: [PATCH] Initial commit Start a new clean history from here. --- .gitignore | 2 + Makefile | 38 ++ app/meson.build | 27 ++ app/src/command.c | 54 +++ app/src/command.h | 45 ++ app/src/decoder.c | 165 +++++++ app/src/decoder.h | 22 + app/src/events.h | 3 + app/src/frames.c | 48 ++ app/src/frames.h | 24 + app/src/lockutil.h | 18 + app/src/netutil.c | 30 ++ app/src/netutil.h | 8 + app/src/scrcpy.c | 63 +++ app/src/screen.c | 439 ++++++++++++++++++ app/src/screen.h | 8 + app/src/strutil.c | 33 ++ app/src/strutil.h | 14 + app/src/unix/command.c | 39 ++ app/src/win/command.c | 44 ++ server/.gitignore | 4 + server/Makefile | 59 +++ server/src/android/view/IRotationWatcher.aidl | 25 + .../genymobile/scrcpy/DesktopConnection.java | 52 +++ .../com/genymobile/scrcpy/ScrCpyServer.java | 29 ++ .../src/com/genymobile/scrcpy/ScreenInfo.java | 26 ++ .../com/genymobile/scrcpy/ScreenStreamer.java | 46 ++ .../scrcpy/ScreenStreamerSession.java | 62 +++ .../src/com/genymobile/scrcpy/ScreenUtil.java | 110 +++++ 29 files changed, 1537 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 app/meson.build create mode 100644 app/src/command.c create mode 100644 app/src/command.h create mode 100644 app/src/decoder.c create mode 100644 app/src/decoder.h create mode 100644 app/src/events.h create mode 100644 app/src/frames.c create mode 100644 app/src/frames.h create mode 100644 app/src/lockutil.h create mode 100644 app/src/netutil.c create mode 100644 app/src/netutil.h create mode 100644 app/src/scrcpy.c create mode 100644 app/src/screen.c create mode 100644 app/src/screen.h create mode 100644 app/src/strutil.c create mode 100644 app/src/strutil.h create mode 100644 app/src/unix/command.c create mode 100644 app/src/win/command.c create mode 100644 server/.gitignore create mode 100644 server/Makefile create mode 100644 server/src/android/view/IRotationWatcher.aidl create mode 100644 server/src/com/genymobile/scrcpy/DesktopConnection.java create mode 100644 server/src/com/genymobile/scrcpy/ScrCpyServer.java create mode 100644 server/src/com/genymobile/scrcpy/ScreenInfo.java create mode 100644 server/src/com/genymobile/scrcpy/ScreenStreamer.java create mode 100644 server/src/com/genymobile/scrcpy/ScreenStreamerSession.java create mode 100644 server/src/com/genymobile/scrcpy/ScreenUtil.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..76af3d4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +/dist diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..df9f5df5 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +.PHONY: default release clean build-app build-server dist + +BUILD_DIR := build +DIST := dist +TARGET_DIR := scrcpy + +VERSION := $(shell git describe --tags --always) +TARGET := $(TARGET_DIR)-$(VERSION).zip + +default: + @echo 'You must specify a target. Try: make release' + +release: clean dist-zip sums + +clean: + rm -rf "$(BUILD_DIR)" "$(DIST)" + +$(MAKE) -C server clean + +build-app: + [ -d "$(BUILD_DIR)" ] || ( mkdir "$(BUILD_DIR)" && meson app "$(BUILD_DIR)" --buildtype release ) + ninja -C "$(BUILD_DIR)" + +build-server: + +$(MAKE) -C server clean + +$(MAKE) -C server jar + +dist: build-app build-server + mkdir -p "$(DIST)/$(TARGET_DIR)" + cp server/scrcpy-server.jar "$(DIST)/$(TARGET_DIR)/" + cp build/scrcpy "$(DIST)/$(TARGET_DIR)/" + +dist-zip: dist + cd "$(DIST)"; \ + zip -r "$(TARGET)" "$(TARGET_DIR)" + +sums: + cd "$(DIST)"; \ + sha256sum *.zip > SHA256SUM.txt diff --git a/app/meson.build b/app/meson.build new file mode 100644 index 00000000..f43a6bd6 --- /dev/null +++ b/app/meson.build @@ -0,0 +1,27 @@ +project('scrcpy-app', 'c') + +src = [ + 'src/scrcpy.c', + 'src/command.c', + 'src/decoder.c', + 'src/frames.c', + 'src/netutil.c', + 'src/screen.c', + 'src/strutil.c', +] + +if host_machine.system() == 'windows' + src += [ 'src/win/command.c' ] +else + src += [ 'src/unix/command.c' ] +endif + +dependencies = [ + dependency('libavformat'), + dependency('libavcodec'), + dependency('libavutil'), + dependency('sdl2'), + dependency('SDL2_net'), +] + +executable('scrcpy', src, dependencies: dependencies) diff --git a/app/src/command.c b/app/src/command.c new file mode 100644 index 00000000..20387823 --- /dev/null +++ b/app/src/command.c @@ -0,0 +1,54 @@ +#include "command.h" + +#include +#include +#include + +#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0])) + +process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) { + const char *cmd[len + 4]; + int i; + cmd[0] = "adb"; + if (serial) { + cmd[1] = "-s"; + cmd[2] = serial; + i = 3; + } else { + i = 1; + } + + memcpy(&cmd[i], adb_cmd, len * sizeof(const char *)); + cmd[len + i] = NULL; + return cmd_execute(cmd[0], cmd); +} + +process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + sprintf(local, "tcp:%" PRIu16, local_port); + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"forward", local, remote}; + 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) { + char local[4 + 5 + 1]; // tcp:PORT + char remote[108 + 14 + 1]; // localabstract:NAME + sprintf(local, "tcp:%" PRIu16, local_port); + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"reverse", remote, local}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t adb_reverse_remove(const char *serial, const char *device_socket_name) { + char remote[108 + 14 + 1]; // localabstract:NAME + snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); + const char *const adb_cmd[] = {"reverse", "--remove", remote}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} + +process_t adb_push(const char *serial, const char *local, const char *remote) { + const char *const adb_cmd[] = {"push", (char *) local, (char *) remote}; + return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd)); +} diff --git a/app/src/command.h b/app/src/command.h new file mode 100644 index 00000000..8a19cd26 --- /dev/null +++ b/app/src/command.h @@ -0,0 +1,45 @@ +#ifndef COMMAND_H +#define COMMAND_H + +#include +#include +#include + +// +#ifdef _WIN32 +# ifdef _WIN64 +# define PRIsizet PRIu64 +# define PRIexitcode "lu" +# else +# define PRIsizet PRIu32 +# define PRIexitcode "u" +# endif +#else +# define PRIsizet "zu" +# define PRIexitcode "d" +#endif + +#ifdef __WINDOWS__ +# include +# define PROCESS_NONE NULL + typedef HANDLE process_t; + typedef DWORD exit_code_t; +#else +# include +# define PROCESS_NONE -1 + typedef pid_t process_t; + typedef int exit_code_t; +#endif +# define NO_EXIT_CODE -1 + +process_t cmd_execute(const char *path, const char *const argv[]); +SDL_bool cmd_terminate(process_t pid); +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_forward(const char *serial, uint16_t local_port, const char *device_socket_name); +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_push(const char *serial, const char *local, const char *remote); + +#endif diff --git a/app/src/decoder.c b/app/src/decoder.c new file mode 100644 index 00000000..d74b4d1d --- /dev/null +++ b/app/src/decoder.c @@ -0,0 +1,165 @@ +#include "decoder.h" + +#include +#include +#include +#include +#include + +#include "events.h" +#include "frames.h" +#include "lockutil.h" +#include "netutil.h" + +#define BUFSIZE 0x10000 + +static int read_packet(void *opaque, uint8_t *buf, int buf_size) { + struct decoder *decoder = opaque; + return SDLNet_TCP_Recv(decoder->video_socket, buf, buf_size); +} + +// set the decoded frame as ready for rendering, and notify +static void push_frame(struct decoder *decoder) { + struct frames *frames = decoder->frames; + lock_mutex(frames->mutex); + if (!decoder->skip_frames) { + while (!frames->rendering_frame_consumed) { + SDL_CondWait(frames->rendering_frame_consumed_cond, frames->mutex); + } + } else if (!frames->rendering_frame_consumed) { + SDL_LogInfo(SDL_LOG_CATEGORY_RENDER, "Skip frame"); + } + + frames_swap(frames); + frames->rendering_frame_consumed = SDL_FALSE; + unlock_mutex(frames->mutex); + + static SDL_Event new_frame_event = { + .type = EVENT_NEW_FRAME, + }; + SDL_PushEvent(&new_frame_event); +} + +static int run_decoder(void *data) { + struct decoder *decoder = data; + int ret = 0; + + AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if (!codec) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "H.264 decoder not found"); + return -1; + } + + AVCodecContext *codec_ctx = avcodec_alloc_context3(codec); + if (!codec_ctx) { + SDL_LogCritical(SDL_LOG_CATEGORY_VIDEO, "Could not allocate decoder context"); + return -1; + } + + if (avcodec_open2(codec_ctx, codec, NULL) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not open H.264 codec"); + ret = -1; + goto run_finally_free_codec_ctx; + } + + AVFormatContext *format_ctx = avformat_alloc_context(); + if (!format_ctx) { + SDL_LogCritical(SDL_LOG_CATEGORY_VIDEO, "Could not allocate format context"); + ret = -1; + goto run_finally_close_codec; + } + + unsigned char *buffer = av_malloc(BUFSIZE); + if (!buffer) { + SDL_LogCritical(SDL_LOG_CATEGORY_VIDEO, "Could not allocate buffer"); + ret = -1; + goto run_finally_free_format_ctx; + } + + AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL); + if (!avio_ctx) { + SDL_LogCritical(SDL_LOG_CATEGORY_VIDEO, "Could not allocate avio context"); + // avformat_open_input takes ownership of 'buffer' + // so only free the buffer before avformat_open_input() + av_free(buffer); + ret = -1; + goto run_finally_free_format_ctx; + } + + format_ctx->pb = avio_ctx; + + //const char *url = "tcp://127.0.0.1:1234"; + if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not open video stream"); + ret = -1; + goto run_finally_free_avio_ctx; + } + + AVPacket packet; + av_init_packet(&packet); + packet.data = NULL; + packet.size = 0; + + while (!av_read_frame(format_ctx, &packet)) { +// the new decoding/encoding API has been introduced by: +// +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(57, 37, 0) + while (packet.size > 0) { + int got_picture; + int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet); + if (len < 0) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not decode video packet: %d", len); + goto run_quit; + } + if (got_picture) { + push_frame(decoder); + } + packet.size -= len; + packet.data += len; + } +#else + int ret; + if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not send video packet: %d", ret); + goto run_quit; + } + if ((ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame)) < 0) { + SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Could not receive video frame: %d", ret); + goto run_quit; + } + + push_frame(decoder); +#endif + } + + SDL_LogDebug(SDL_LOG_CATEGORY_VIDEO, "End of frames"); + +run_quit: + avformat_close_input(&format_ctx); +run_finally_free_avio_ctx: + av_freep(&avio_ctx); +run_finally_free_format_ctx: + avformat_free_context(format_ctx); +run_finally_close_codec: + avcodec_close(codec_ctx); +run_finally_free_codec_ctx: + avcodec_free_context(&codec_ctx); + SDL_PushEvent(&(SDL_Event) {.type = EVENT_DECODER_STOPPED}); + return ret; +} + +int decoder_start(struct decoder *decoder) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Starting decoder thread"); + + decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder); + if (!decoder->thread) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not start decoder thread"); + return -1; + } + + return 0; +} + +void decoder_join(struct decoder *decoder) { + SDL_WaitThread(decoder->thread, NULL); +} diff --git a/app/src/decoder.h b/app/src/decoder.h new file mode 100644 index 00000000..78f2dbd7 --- /dev/null +++ b/app/src/decoder.h @@ -0,0 +1,22 @@ +#ifndef DECODER_H +#define DECODER_H + +#include +#include + +struct frames; +typedef struct SDL_Thread SDL_Thread; +typedef struct SDL_mutex SDL_mutex; + +struct decoder { + struct frames *frames; + TCPsocket video_socket; + SDL_Thread *thread; + SDL_mutex *mutex; + SDL_bool skip_frames; +}; + +int decoder_start(struct decoder *decoder); +void decoder_join(struct decoder *decoder); + +#endif diff --git a/app/src/events.h b/app/src/events.h new file mode 100644 index 00000000..ff0c1a05 --- /dev/null +++ b/app/src/events.h @@ -0,0 +1,3 @@ +#define EVENT_NEW_SESSION SDL_USEREVENT +#define EVENT_NEW_FRAME (SDL_USEREVENT + 1) +#define EVENT_DECODER_STOPPED (SDL_USEREVENT + 2) diff --git a/app/src/frames.c b/app/src/frames.c new file mode 100644 index 00000000..ea2e2daf --- /dev/null +++ b/app/src/frames.c @@ -0,0 +1,48 @@ +#include "frames.h" + +#include +#include +#include + +int frames_init(struct frames *frames) { + if (!(frames->decoding_frame = av_frame_alloc())) { + goto error_0; + } + + if (!(frames->rendering_frame = av_frame_alloc())) { + goto error_1; + } + + if (!(frames->mutex = SDL_CreateMutex())) { + goto error_2; + } + + if (!(frames->rendering_frame_consumed_cond = SDL_CreateCond())) { + goto error_3; + } + + frames->rendering_frame_consumed = SDL_TRUE; + + return 0; + +error_3: + SDL_DestroyMutex(frames->mutex); +error_2: + av_frame_free(&frames->rendering_frame); +error_1: + av_frame_free(&frames->decoding_frame); +error_0: + return -1; +} + +void frames_destroy(struct frames *frames) { + SDL_DestroyMutex(frames->mutex); + av_frame_free(&frames->rendering_frame); + av_frame_free(&frames->decoding_frame); +} + +void frames_swap(struct frames *frames) { + AVFrame *tmp = frames->decoding_frame; + frames->decoding_frame = frames->rendering_frame; + frames->rendering_frame = tmp; +} diff --git a/app/src/frames.h b/app/src/frames.h new file mode 100644 index 00000000..cf2217d9 --- /dev/null +++ b/app/src/frames.h @@ -0,0 +1,24 @@ +#ifndef FRAMES_H +#define FRAMES_H + +#include + +// forward declarations +typedef struct AVFrame AVFrame; +typedef struct SDL_mutex SDL_mutex; +typedef struct SDL_cond SDL_cond; + +struct frames { + AVFrame *decoding_frame; + AVFrame *rendering_frame; + SDL_mutex *mutex; + SDL_cond *rendering_frame_consumed_cond; + SDL_bool rendering_frame_consumed; +}; + +int frames_init(struct frames *frames); +void frames_destroy(struct frames *frames); + +void frames_swap(struct frames *frames); + +#endif diff --git a/app/src/lockutil.h b/app/src/lockutil.h new file mode 100644 index 00000000..1988eecd --- /dev/null +++ b/app/src/lockutil.h @@ -0,0 +1,18 @@ +#ifndef LOCKUTIL_H +#define LOCKUTIL_H + +static inline void lock_mutex(SDL_mutex *mutex) { + if (SDL_LockMutex(mutex)) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not lock mutex"); + exit(1); + } +} + +static inline void unlock_mutex(SDL_mutex *mutex) { + if (SDL_UnlockMutex(mutex)) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not unlock mutex"); + exit(1); + } +} + +#endif diff --git a/app/src/netutil.c b/app/src/netutil.c new file mode 100644 index 00000000..8558edbb --- /dev/null +++ b/app/src/netutil.c @@ -0,0 +1,30 @@ +#include "netutil.h" + +#include + +// contrary to SDLNet_TCP_Send and SDLNet_TCP_Recv, SDLNet_TCP_Accept is non-blocking +// so we need to block before calling it +TCPsocket blocking_accept(TCPsocket server_socket) { + SDLNet_SocketSet set = SDLNet_AllocSocketSet(1); + if (!set) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not allocate socket set"); + return NULL; + } + + if (SDLNet_TCP_AddSocket(set, server_socket) == -1) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not add socket to set"); + SDLNet_FreeSocketSet(set); + return NULL; + } + + // timeout is (2^32-1) milliseconds, this should be sufficient + if (SDLNet_CheckSockets(set, -1) != 1) { + SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Could not check socket: %s", SDL_GetError()); + SDLNet_FreeSocketSet(set); + return NULL; + } + + SDLNet_FreeSocketSet(set); + + return SDLNet_TCP_Accept(server_socket); +} diff --git a/app/src/netutil.h b/app/src/netutil.h new file mode 100644 index 00000000..3c2659ba --- /dev/null +++ b/app/src/netutil.h @@ -0,0 +1,8 @@ +#ifndef NETUTIL_H +#define NETUTIL_H + +#include + +TCPsocket blocking_accept(TCPsocket server_socket); + +#endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c new file mode 100644 index 00000000..df7b80fd --- /dev/null +++ b/app/src/scrcpy.c @@ -0,0 +1,63 @@ +#include "screen.h" + +#include +#include +#include + +#define DEFAULT_LOCAL_PORT 27183 + +struct args { + const char *serial; + Uint16 port; +}; + +int parse_args(struct args *args, int argc, char *argv[]) { + int c; + while ((c = getopt(argc, argv, "p:")) != -1) { + switch (c) { + case 'p': { + char *endptr; + long int value = strtol(optarg, &endptr, 0); + if (*optarg == '\0' || *endptr != '\0') { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid port: %s\n", optarg); + return -1; + } + if (value & ~0xffff) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Port out of range: %ld\n", value); + return -1; + } + args->port = (Uint16) value; + break; + } + default: + // getopt prints the error message on stderr + return -1; + } + } + + int index = optind; + if (index < argc) { + args->serial = argv[index++]; + } + if (index < argc) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unexpected additional argument: %s\n", argv[index]); + return -1; + } + return 0; +} + +int main(int argc, char *argv[]) { + av_register_all(); + avformat_network_init(); + SDL_LogSetAllPriority(SDL_LOG_PRIORITY_DEBUG); + + struct args args = { + .serial = NULL, + .port = DEFAULT_LOCAL_PORT, + }; + if (parse_args(&args, argc, argv)) { + return -1; + } + + return show_screen(args.serial, args.port); +} diff --git a/app/src/screen.c b/app/src/screen.c new file mode 100644 index 00000000..d5142dfb --- /dev/null +++ b/app/src/screen.c @@ -0,0 +1,439 @@ +#include "screen.h" + +#include +#include +#include +#include +#include +#include + +#include "command.h" +#include "decoder.h" +#include "events.h" +#include "frames.h" +#include "lockutil.h" +#include "netutil.h" + +#define SOCKET_NAME "scrcpy" +#define DISPLAY_MARGINS 96 +#define MIN(X,Y) (X) < (Y) ? (X) : (Y) +#define MAX(X,Y) (X) > (Y) ? (X) : (Y) + +static struct frames frames; +static struct decoder decoder; + +struct size { + Uint16 width; + Uint16 height; +}; + +static long timestamp_ms() { + struct timeval tv; + gettimeofday(&tv, NULL); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; +} + +static TCPsocket listen_on_port(Uint16 port) { + IPaddress addr = { + .host = INADDR_ANY, + .port = SDL_SwapBE16(port), + }; + return SDLNet_TCP_Open(&addr); +} + +static process_t start_server(const char *serial) { + const char *const cmd[] = { + "shell", + "CLASSPATH=/data/local/tmp/scrcpy-server.jar", + "app_process", + "/system/bin", + "com.genymobile.scrcpy.ScrCpyServer" + }; + return adb_execute(serial, cmd, sizeof(cmd) / sizeof(cmd[0])); +} + +static void stop_server(process_t server) { + if (!cmd_terminate(server)) { + SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Could not kill: %s", strerror(errno)); + } +} + +SDL_bool read_initial_device_size(TCPsocket socket, struct size *size) { + unsigned char buf[4]; + if (SDLNet_TCP_Recv(socket, buf, sizeof(buf)) <= 0) { + return SDL_FALSE; + } + size->width = (buf[0] << 8) | buf[1]; + size->height = (buf[2] << 8) | buf[3]; + return SDL_TRUE; +} + +#if SDL_VERSION_ATLEAST(2, 0, 5) +# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayUsableBounds((i), (r)) +#else +# define GET_DISPLAY_BOUNDS(i, r) SDL_GetDisplayBounds((i), (r)) +#endif + +// init the preferred display_bounds (i.e. the screen bounds with some margins) +static SDL_bool get_preferred_display_bounds(struct size *bounds) { + SDL_Rect rect; + if (GET_DISPLAY_BOUNDS(0, &rect)) { + SDL_LogWarn(SDL_LOG_CATEGORY_SYSTEM, "Could not get display usable bounds: %s", SDL_GetError()); + return SDL_FALSE; + } + + bounds->width = MAX(0, rect.w - DISPLAY_MARGINS); + bounds->height = MAX(0, rect.h - DISPLAY_MARGINS); + return SDL_TRUE; +} + +static inline struct size get_window_size(SDL_Window *window) { + int width; + int height; + SDL_GetWindowSize(window, &width, &height); + + struct size size; + size.width = width; + size.height = height; + return size; +} + +// return the optimal size of the window, with the following constraints: +// - it attempts to keep at least one dimension of the current_size (i.e. it crops the black borders) +// - it keeps the aspect ratio +// - it scales down to make it fit in the display_size +// TODO unit test +static struct size get_optimal_size(struct size current_size, struct size frame_size) { + struct size display_size; + // 32 bits because we need to multiply two 16 bits values + Uint32 w; + Uint32 h; + + if (!get_preferred_display_bounds(&display_size)) { + // cannot get display bounds, do not constraint the size + w = current_size.width; + h = current_size.height; + } else { + w = MIN(current_size.width, display_size.width); + h = MIN(current_size.height, display_size.height); + } + + SDL_bool keep_width = frame_size.width * h > frame_size.height * w; + if (keep_width) { + // remove black borders on top and bottom + h = frame_size.height * w / frame_size.width; + } else { + // remove black borders on left and right (or none at all if it already fits) + w = frame_size.width * h / frame_size.height; + } + + // w and h must fit into 16 bits + SDL_assert_release(!(w & ~0xffff) && !(h & ~0xffff)); + return (struct size) {w, h}; +} + +// initially, there is no current size, so use the frame size as current size +static inline struct size get_initial_optimal_size(struct size frame_size) { + return get_optimal_size(frame_size, frame_size); +} + +// same as get_optimal_size(), but read the current size from the window +static inline struct size get_optimal_window_size(SDL_Window *window, struct size frame_size) { + struct size current_size = get_window_size(window); + return get_optimal_size(current_size, frame_size); +} + +static inline SDL_bool prepare_for_frame(SDL_Window *window, SDL_Renderer *renderer, SDL_Texture **texture, + struct size old_frame_size, struct size frame_size) { + (void) window; // might be used to resize the window automatically + if (old_frame_size.width != frame_size.width || old_frame_size.height != frame_size.height) { + if (SDL_RenderSetLogicalSize(renderer, frame_size.width, frame_size.height)) { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Could not set renderer logical size: %s", SDL_GetError()); + return SDL_FALSE; + } + + // frame dimension changed, destroy texture + SDL_DestroyTexture(*texture); + + struct size current_size = get_window_size(window); + struct size target_size = { + (Uint32) current_size.width * frame_size.width / old_frame_size.width, + (Uint32) current_size.height * frame_size.height / old_frame_size.height, + }; + target_size = get_optimal_size(target_size, frame_size); + SDL_SetWindowSize(window, target_size.width, target_size.height); + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "New texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); + *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, frame_size.width, frame_size.height); + if (!*texture) { + SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Could not create texture: %s", SDL_GetError()); + return SDL_FALSE; + } + } + + return SDL_TRUE; +} + + +static void update_texture(AVFrame *frame, SDL_Texture *texture) { + SDL_UpdateYUVTexture(texture, NULL, + frame->data[0], frame->linesize[0], + frame->data[1], frame->linesize[1], + frame->data[2], frame->linesize[2]); +} + +static void render(SDL_Renderer *renderer, SDL_Texture *texture) { + SDL_RenderClear(renderer); + if (texture) { + SDL_RenderCopy(renderer, texture, NULL, NULL); + } + SDL_RenderPresent(renderer); +} + +static int wait_for_success(process_t proc, const char *name) { + if (proc == PROCESS_NONE) { + SDL_LogError(SDL_LOG_CATEGORY_SYSTEM, "Could not execute \"%s\"", name); + return -1; + } + exit_code_t exit_code; + if (!cmd_simple_wait(proc, &exit_code)) { + if (exit_code != NO_EXIT_CODE) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "\"%s\" returned with value %" PRIexitcode, name, exit_code); + } else { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "\"%s\" exited unexpectedly", name); + } + return -1; + } + return 0; +} + +int show_screen(const char *serial, Uint16 local_port) { + int ret = 0; + + const char *server_jar_path = getenv("SCRCPY_SERVER_JAR"); + if (!server_jar_path) { + server_jar_path = "scrcpy-server.jar"; + } + process_t push_proc = adb_push(serial, server_jar_path, "/data/local/tmp/"); + if (wait_for_success(push_proc, "adb push")) { + return -1; + } + + process_t reverse_tunnel_proc = adb_reverse(serial, SOCKET_NAME, local_port); + if (wait_for_success(reverse_tunnel_proc, "adb reverse")) { + return -1; + } + + TCPsocket server_socket = listen_on_port(local_port); + if (!server_socket) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Could not open video socket"); + goto screen_finally_adb_reverse_remove; + } + + // server will connect to our socket + process_t server = start_server(serial); + if (server == PROCESS_NONE) { + ret = -1; + SDLNet_TCP_Close(server_socket); + goto screen_finally_adb_reverse_remove; + } + + // to reduce startup time, we could be tempted to init other stuff before blocking here + // but we should not block after SDL_Init since it handles the signals (Ctrl+C) in its + // event loop: blocking could lead to deadlock + TCPsocket device_socket = blocking_accept(server_socket); + // we don't need the server socket anymore + SDLNet_TCP_Close(server_socket); + if (!device_socket) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Could not accept video socket: %s", SDL_GetError()); + ret = -1; + stop_server(server); + goto screen_finally_adb_reverse_remove; + } + + struct size frame_size; + + // screenrecord does not send frames when the screen content does not change + // therefore, we transmit the screen size before the video stream, to be able + // to init the window immediately + if (!read_initial_device_size(device_socket, &frame_size)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Could not retrieve initial screen size"); + ret = -1; + SDLNet_TCP_Close(device_socket); + stop_server(server); + goto screen_finally_adb_reverse_remove; + } + + if (frames_init(&frames)) { + ret = -1; + SDLNet_TCP_Close(device_socket); + stop_server(server); + goto screen_finally_adb_reverse_remove; + } + + decoder.frames = &frames; + decoder.video_socket = device_socket; + decoder.skip_frames = SDL_TRUE; + + // now we consumed the width and height values, the socket receives the video stream + // start the decoder + if (decoder_start(&decoder)) { + ret = -1; + SDLNet_TCP_Close(device_socket); + stop_server(server); + goto screen_finally_destroy_frames; + } + + if (SDL_Init(SDL_INIT_VIDEO)) { + SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "Could not initialize SDL: %s", SDL_GetError()); + ret = -1; + goto screen_finally_stop_decoder; + } + atexit(SDL_Quit); + + // Bilinear resizing + if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { + SDL_LogWarn(SDL_LOG_CATEGORY_VIDEO, "Could not enable bilinear filtering"); + } + + struct size window_size = get_initial_optimal_size(frame_size); + SDL_Window *window = SDL_CreateWindow("scrcpy", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, + window_size.width, window_size.height, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE); + if (!window) { + SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "Could not create window: %s", SDL_GetError()); + ret = -1; + goto screen_finally_stop_decoder; + } + + SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); + if (!renderer) { + SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Could not create renderer: %s", SDL_GetError()); + ret = -1; + goto screen_finally_destroy_window; + } + + if (SDL_RenderSetLogicalSize(renderer, frame_size.width, frame_size.height)) { + SDL_LogError(SDL_LOG_CATEGORY_RENDER, "Could not set renderer logical size: %s", SDL_GetError()); + ret = -1; + goto screen_finally_destroy_renderer; + } + + SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "Initial texture: %" PRIu16 "x%" PRIu16, frame_size.width, frame_size.height); + SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, frame_size.width, frame_size.height); + if (!texture) { + SDL_LogCritical(SDL_LOG_CATEGORY_RENDER, "Could not create texture: %s", SDL_GetError()); + ret = -1; + goto screen_finally_destroy_renderer; + } + + SDL_RenderClear(renderer); + SDL_RenderPresent(renderer); + + long ts = timestamp_ms(); + int nbframes = 0; + + SDL_bool texture_empty = SDL_TRUE; + SDL_bool fullscreen = SDL_FALSE; + SDL_Event event; + while (SDL_WaitEvent(&event)) { + switch (event.type) { + case EVENT_DECODER_STOPPED: + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "Video decoder stopped"); + case SDL_QUIT: + goto screen_quit; + case EVENT_NEW_FRAME: + lock_mutex(frames.mutex); + AVFrame *frame = frames.rendering_frame; + frames.rendering_frame_consumed = SDL_TRUE; + if (!decoder.skip_frames) { + SDL_CondSignal(frames.rendering_frame_consumed_cond); + } + + struct size current_frame_size = {frame->width, frame->height}; + if (!prepare_for_frame(window, renderer, &texture, frame_size, current_frame_size)) { + goto screen_quit; + } + + frame_size = current_frame_size; + + update_texture(frame, texture); + unlock_mutex(frames.mutex); + + texture_empty = SDL_FALSE; + + long now = timestamp_ms(); + ++nbframes; + if (now - ts > 1000) { + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "%d fps", nbframes); + ts = now; + nbframes = 0; + } + render(renderer, texture); + + break; + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_SIZE_CHANGED: + render(renderer, texture_empty ? NULL : texture); + break; + } + break; + case SDL_KEYDOWN: { + SDL_bool ctrl = SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL); + SDL_bool shift = SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT); + SDL_bool repeat = event.key.repeat; + switch (event.key.keysym.sym) { + case SDLK_x: + if (!repeat && ctrl && !shift) { + // Ctrl+x + struct size optimal_size = get_optimal_window_size(window, frame_size); + SDL_SetWindowSize(window, optimal_size.width, optimal_size.height); + } + break; + case SDLK_f: + if (!repeat && ctrl && !shift) { + // Ctrl+f + Uint32 new_mode = fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; + if (!SDL_SetWindowFullscreen(window, new_mode)) { + fullscreen = !fullscreen; + render(renderer, texture_empty ? NULL : texture); + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Could not switch fullscreen mode: %s", SDL_GetError()); + } + } + break; + } + break; + } + } + } + +screen_quit: + SDL_LogDebug(SDL_LOG_CATEGORY_APPLICATION, "quit..."); + SDL_DestroyTexture(texture); +screen_finally_destroy_renderer: + // FIXME it may crash at exit if we destroy the renderer or the window, + // with the exact same stack trace as . + // As a workaround, leak the renderer and the window (we are exiting anyway). + //SDL_DestroyRenderer(renderer); +screen_finally_destroy_window: + //SDL_DestroyWindow(window); +screen_finally_stop_decoder: + SDLNet_TCP_Close(device_socket); + // kill the server before decoder_join() to wake up the decoder + stop_server(server); + decoder_join(&decoder); +screen_finally_destroy_frames: + frames_destroy(&frames); +screen_finally_adb_reverse_remove: + { + process_t remove = adb_reverse_remove(serial, SOCKET_NAME); + if (remove != PROCESS_NONE) { + // ignore failure + cmd_simple_wait(remove, NULL); + } + } + + return ret; +} diff --git a/app/src/screen.h b/app/src/screen.h new file mode 100644 index 00000000..5b517480 --- /dev/null +++ b/app/src/screen.h @@ -0,0 +1,8 @@ +#ifndef SCREEN_H +#define SCREEN_H + +#include + +int show_screen(const char *serial, Uint16 local_port); + +#endif diff --git a/app/src/strutil.c b/app/src/strutil.c new file mode 100644 index 00000000..0b5ac12d --- /dev/null +++ b/app/src/strutil.c @@ -0,0 +1,33 @@ +#include "strutil.h" + +size_t xstrncpy(char *dest, const char *src, size_t n) { + size_t i; + for (i = 0; i < n - 1 && src[i] != '\0'; ++i) + dest[i] = src[i]; + if (n) + dest[i] = '\0'; + return src[i] == '\0' ? i : n; +} + +size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t n) { + const char *const *remaining = tokens; + const char *token = *remaining++; + size_t i = 0; + while (token) { + if (i) { + dst[i++] = sep; + if (i == n) + goto truncated; + } + size_t w = xstrncpy(dst + i, token, n - i); + if (w >= n - i) + goto truncated; + i += w; + token = *remaining++; + } + return i; + +truncated: + dst[n - 1] = '\0'; + return n; +} diff --git a/app/src/strutil.h b/app/src/strutil.h new file mode 100644 index 00000000..76771781 --- /dev/null +++ b/app/src/strutil.h @@ -0,0 +1,14 @@ +#include + +// like strncpy, except: +// - it copies at most n-1 chars +// - the dest string is nul-terminated +// - it does not write useless bytes if strlen(src) < n +// - it returns the number of chars actually written (max n-1) if src has +// been copied completely, or n if src has been truncated +size_t xstrncpy(char *dest, const char *src, size_t n); + +// join tokens by sep into dst +// returns the number of chars actually written (max n-1) if no trucation +// occurred, or n if truncated +size_t xstrjoin(char *dst, const char *const tokens[], char sep, size_t); diff --git a/app/src/unix/command.c b/app/src/unix/command.c new file mode 100644 index 00000000..70dbd91b --- /dev/null +++ b/app/src/unix/command.c @@ -0,0 +1,39 @@ +#include "../command.h" + +#include +#include +#include +#include + +pid_t cmd_execute(const char *path, const char *const argv[]) { + pid_t pid = fork(); + if (pid == -1) { + perror("fork"); + return -1; + } + if (pid == 0) { + execvp(path, (char *const *)argv); + perror("exec"); + exit(1); + } + return pid; +} + +SDL_bool cmd_terminate(pid_t pid) { + return kill(pid, SIGTERM) != -1; +} + +SDL_bool cmd_simple_wait(pid_t pid, int *exit_code) { + int status; + int code; + if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) { + // cannot wait, or exited unexpectedly, probably by a signal + code = -1; + } else { + code = WEXITSTATUS(status); + } + if (exit_code) { + *exit_code = code; + } + return !code; +} diff --git a/app/src/win/command.c b/app/src/win/command.c new file mode 100644 index 00000000..ad9984e0 --- /dev/null +++ b/app/src/win/command.c @@ -0,0 +1,44 @@ +#include "../command.h" + +#include +#include "../strutil.h" + +HANDLE cmd_execute(const char *path, const char *const argv[]) { + STARTUPINFO si; + PROCESS_INFORMATION pi; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + + // Windows command-line parsing is WTF: + // + // only make it work for this very specific program + // (don't handle escaping nor quotes) + char cmd[256]; + size_t ret = xstrjoin(cmd, argv, ' ', sizeof(cmd)); + if (ret >= sizeof(cmd)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Command too long (%" PRIsizet " chars)", sizeof(cmd) - 1); + return NULL; + } + + if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { + return NULL; + } + + return pi.hProcess; +} + +SDL_bool cmd_terminate(HANDLE handle) { + return CloseHandle(handle); +} + +SDL_bool cmd_simple_wait(HANDLE handle, DWORD *exit_code) { + DWORD code; + if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeProcess(handle, &code)) { + // cannot wait or retrieve the exit code + code = -1; // max value, it's unsigned + } + if (exit_code) { + *exit_code = code; + } + return !code; +} diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..e5f088ef --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,4 @@ +*.class +*.jar +*.dex +/gen diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 00000000..efa41dcf --- /dev/null +++ b/server/Makefile @@ -0,0 +1,59 @@ +.PHONY: jar push run clean + +SRC_DIR := src +GEN_DIR := gen +CLS_DIR := classes +CLS_DEX := classes.dex + +BUILD_TOOLS := $(ANDROID_HOME)/build-tools/26.0.2 +AIDL := $(BUILD_TOOLS)/aidl +ifeq ($(OS),Windows_NT) + DX := $(BUILD_TOOLS)/dx.bat +else + DX := $(BUILD_TOOLS)/dx +endif + +ANDROID_JAR := $(ANDROID_HOME)/platforms/android-26/android.jar + +AIDL_SRC := android/view/IRotationWatcher.aidl +SRC := com/genymobile/scrcpy/ScrCpyServer.java \ + com/genymobile/scrcpy/DesktopConnection.java \ + com/genymobile/scrcpy/ScreenInfo.java \ + com/genymobile/scrcpy/ScreenStreamer.java \ + com/genymobile/scrcpy/ScreenStreamerSession.java \ + com/genymobile/scrcpy/ScreenUtil.java \ + +JAR := scrcpy-server.jar +MAIN := com.genymobile.scrcpy.ScrCpyServer + +AIDL_GEN := $(AIDL_SRC:%.aidl=$(GEN_DIR)/%.java) +AIDL_CLS := $(AIDL_SRC:%.aidl=$(CLS_DIR)/%.class) +SRC_CLS := $(SRC:%.java=$(CLS_DIR)/%.class) +CLS := $(AIDL_CLS) $(SRC_CLS) + +ALL_JAVA := $(AIDL_GEN) $(addprefix $(SRC_DIR)/,$(SRC)) + +jar: $(JAR) + +$(AIDL_GEN): $(GEN_DIR)/%.java : $(SRC_DIR)/%.aidl + mkdir -p $(GEN_DIR) + "$(AIDL)" -o$(GEN_DIR) $(SRC_DIR)/$(AIDL_SRC) + + +$(JAR): $(ALL_JAVA) + @mkdir -p $(CLS_DIR) + javac -source 1.7 -target 1.7 \ + -cp "$(ANDROID_JAR)" \ + -d "$(CLS_DIR)" -sourcepath $(SRC_DIR):$(GEN_DIR) \ + $(ALL_JAVA) + "$(DX)" --dex --output=$(CLS_DEX) $(CLS_DIR) + jar cvf $(JAR) classes.dex + +push: jar + adb push $(JAR) /data/local/tmp/ + +run: push + adb shell "CLASSPATH=/data/local/tmp/$(JAR) app_process /system/bin $(MAIN)" + +clean: + rm -rf $(CLS_DEX) $(CLS_DIR) $(GEN_DIR) $(JAR) diff --git a/server/src/android/view/IRotationWatcher.aidl b/server/src/android/view/IRotationWatcher.aidl new file mode 100644 index 00000000..2c83642f --- /dev/null +++ b/server/src/android/view/IRotationWatcher.aidl @@ -0,0 +1,25 @@ +/* //device/java/android/android/hardware/ISensorListener.aidl +** +** Copyright 2008, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.view; + +/** + * {@hide} + */ +oneway interface IRotationWatcher { + void onRotationChanged(int rotation); +} diff --git a/server/src/com/genymobile/scrcpy/DesktopConnection.java b/server/src/com/genymobile/scrcpy/DesktopConnection.java new file mode 100644 index 00000000..07a8dd4a --- /dev/null +++ b/server/src/com/genymobile/scrcpy/DesktopConnection.java @@ -0,0 +1,52 @@ +package com.genymobile.scrcpy; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; + +import java.io.Closeable; +import java.io.IOException; + +public class DesktopConnection implements Closeable { + + private static final String SOCKET_NAME = "scrcpy"; + + private final LocalSocket socket; + + private DesktopConnection(LocalSocket socket) throws IOException { + this.socket = socket; + } + + private static LocalSocket connect(String abstractName) throws IOException { + LocalSocket localSocket = new LocalSocket(); + localSocket.connect(new LocalSocketAddress(abstractName)); + return localSocket; + } + + public static DesktopConnection open(int width, int height) throws IOException { + LocalSocket socket = connect(SOCKET_NAME); + send(socket, width, height); + return new DesktopConnection(socket); + } + + public void close() throws IOException { + socket.shutdownInput(); + socket.shutdownOutput(); + socket.close(); + } + + private static void send(LocalSocket socket, int width, int height) throws IOException { + assert width < 0x10000 : "width may not be stored on 16 bits"; + assert height < 0x10000 : "height may not be stored on 16 bits"; + byte[] buffer = new byte[4]; + buffer[0] = (byte) (width >> 8); + buffer[1] = (byte) width; + buffer[2] = (byte) (height >> 8); + buffer[3] = (byte) height; + socket.getOutputStream().write(buffer, 0, 4); + } + + public void sendVideoStream(byte[] videoStreamBuffer, int len) throws IOException { + socket.getOutputStream().write(videoStreamBuffer, 0, len); + } +} + diff --git a/server/src/com/genymobile/scrcpy/ScrCpyServer.java b/server/src/com/genymobile/scrcpy/ScrCpyServer.java new file mode 100644 index 00000000..d31fd244 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ScrCpyServer.java @@ -0,0 +1,29 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; + +public class ScrCpyServer { + + public static void scrcpy() throws IOException { + ScreenInfo initialScreenInfo = ScreenUtil.getScreenInfo(); + int width = initialScreenInfo.getLogicalWidth(); + int height = initialScreenInfo.getLogicalHeight(); + try (DesktopConnection connection = DesktopConnection.open(width, height)) { + try { + new ScreenStreamer(connection).streamScreen(); + } catch (IOException e) { + System.err.println("Screen streaming interrupted: " + e.getMessage()); + System.err.flush(); + } + } + } + + public static void main(String... args) throws Exception { + try { + scrcpy(); + } catch (Throwable t) { + t.printStackTrace(); + throw t; + } + } +} diff --git a/server/src/com/genymobile/scrcpy/ScreenInfo.java b/server/src/com/genymobile/scrcpy/ScreenInfo.java new file mode 100644 index 00000000..312142e1 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ScreenInfo.java @@ -0,0 +1,26 @@ +package com.genymobile.scrcpy; + +public class ScreenInfo { + private final int width; + private final int height; + private int rotation; + + public ScreenInfo(int width, int height, int rotation) { + this.width = width; + this.height = height; + this.rotation = rotation; + } + + public ScreenInfo withRotation(int rotation) { + return new ScreenInfo(width, height, rotation); + } + + public int getLogicalWidth() { + return (rotation & 1) == 0 ? width : height; + } + + public int getLogicalHeight() { + return (rotation & 1) == 0 ? height : width; + } +} + diff --git a/server/src/com/genymobile/scrcpy/ScreenStreamer.java b/server/src/com/genymobile/scrcpy/ScreenStreamer.java new file mode 100644 index 00000000..c85cea7e --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ScreenStreamer.java @@ -0,0 +1,46 @@ +package com.genymobile.scrcpy; + +import android.os.RemoteException; +import android.view.IRotationWatcher; + +import java.io.IOException; +import java.io.InterruptedIOException; + +public class ScreenStreamer { + + private final DesktopConnection connection; + private ScreenStreamerSession currentStreamer; // protected by 'this' + + public ScreenStreamer(DesktopConnection connection) { + this.connection = connection; + ScreenUtil.registerRotationWatcher(new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) throws RemoteException { + reset(); + } + }); + } + + private synchronized ScreenStreamerSession newScreenStreamerSession() { + currentStreamer = new ScreenStreamerSession(connection); + return currentStreamer; + } + + public void streamScreen() throws IOException { + while (true) { + try { + ScreenStreamerSession screenStreamer = newScreenStreamerSession(); + screenStreamer.streamScreen(); + } catch (InterruptedIOException e) { + // the current screenrecord process has probably been killed due to reset(), start a new one without failing + } + } + } + + public synchronized void reset() { + if (currentStreamer != null) { + // it will stop the blocking call to streamScreen(), so a new streamer will be started + currentStreamer.stop(); + } + } +} diff --git a/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java b/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java new file mode 100644 index 00000000..6731162d --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ScreenStreamerSession.java @@ -0,0 +1,62 @@ +package com.genymobile.scrcpy; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +public class ScreenStreamerSession { + + private final DesktopConnection connection; + private Process screenRecordProcess; // protected by 'this' + private final AtomicBoolean stopped = new AtomicBoolean(); + private final byte[] buffer = new byte[0x10000]; + + public ScreenStreamerSession(DesktopConnection connection) { + this.connection = connection; + } + + public void streamScreen() throws IOException { + // screenrecord may not record more than 3 minutes, so restart it on EOF + while (!stopped.get() && streamScreenOnce()) ; + } + + /** + * Starts screenrecord once and relay its output to the desktop connection. + * + * @return {@code true} if EOF is reached, {@code false} otherwise (i.e. requested to stop). + * @throws IOException if an I/O error occurred + */ + private boolean streamScreenOnce() throws IOException { + Process process = startScreenRecord(); + setCurrentProcess(process); + InputStream inputStream = process.getInputStream(); + int r; + while ((r = inputStream.read(buffer)) != -1 && !stopped.get()) { + connection.sendVideoStream(buffer, r); + } + return r != -1; + } + + public void stop() { + // let the thread stop itself without breaking the video stream + stopped.set(true); + killCurrentProcess(); + } + + private static Process startScreenRecord() throws IOException { + Process process = new ProcessBuilder("screenrecord", "--output-format=h264", "-").start(); + process.getOutputStream().close(); + return process; + } + + private synchronized void setCurrentProcess(Process screenRecordProcess) { + this.screenRecordProcess = screenRecordProcess; + } + + private synchronized void killCurrentProcess() { + if (screenRecordProcess != null) { + screenRecordProcess.destroy(); + screenRecordProcess = null; + } + } +} diff --git a/server/src/com/genymobile/scrcpy/ScreenUtil.java b/server/src/com/genymobile/scrcpy/ScreenUtil.java new file mode 100644 index 00000000..29e2d5c2 --- /dev/null +++ b/server/src/com/genymobile/scrcpy/ScreenUtil.java @@ -0,0 +1,110 @@ +package com.genymobile.scrcpy; + +import android.os.IBinder; +import android.os.IInterface; +import android.view.IRotationWatcher; + +import java.lang.reflect.Method; + +public class ScreenUtil { + + private static final ServiceManager serviceManager = new ServiceManager(); + + public static ScreenInfo getScreenInfo() { + return serviceManager.getDisplayManager().getScreenInfo(); + } + + public static void registerRotationWatcher(IRotationWatcher rotationWatcher) { + serviceManager.getWindowManager().registerRotationWatcher(rotationWatcher); + } + + private static class ServiceManager { + private Method getServiceMethod; + + public ServiceManager() { + try { + getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private IInterface getService(String service, String type) { + try { + IBinder binder = (IBinder) getServiceMethod.invoke(null, service); + Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class); + return (IInterface) asInterfaceMethod.invoke(null, binder); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public WindowManager getWindowManager() { + return new WindowManager(getService("window", "android.view.IWindowManager")); + } + + public DisplayManager getDisplayManager() { + return new DisplayManager(getService("display", "android.hardware.display.IDisplayManager")); + } + } + + private static class WindowManager { + private IInterface manager; + + public WindowManager(IInterface manager) { + this.manager = manager; + } + + public int getRotation() { + try { + Class cls = manager.getClass(); + try { + return (Integer) manager.getClass().getMethod("getRotation").invoke(manager); + } catch (NoSuchMethodException e) { + // method changed since this commit: + // https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2 + return (Integer) cls.getMethod("getDefaultDisplayRotation").invoke(manager); + } + } catch (Exception e) { + throw new AssertionError(e); + } + } + + public void registerRotationWatcher(IRotationWatcher rotationWatcher) { + try { + Class cls = manager.getClass(); + try { + cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); + } catch (NoSuchMethodException e) { + // display parameter added since this commit: + // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 + cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, 0); + } + } catch (Exception e) { + throw new AssertionError(e); + } + } + } + + private static class DisplayManager { + private IInterface manager; + + public DisplayManager(IInterface manager) { + this.manager = manager; + } + + public ScreenInfo getScreenInfo() { + try { + Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, 0); + Class cls = displayInfo.getClass(); + // width and height do not depend on the rotation + int width = (Integer) cls.getMethod("getNaturalWidth").invoke(displayInfo); + int height = (Integer) cls.getMethod("getNaturalHeight").invoke(displayInfo); + int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); + return new ScreenInfo(width, height, rotation); + } catch (Exception e) { + throw new AssertionError(e); + } + } + } +}