Compare commits

..

3 Commits

Author SHA1 Message Date
Romain Vimont
190a90f45d Request scan media on push
After a file is pushed, send a broadcast to request the media scanner to
scan the "push target" directory.

In practice, this allows some apps to detect the new file immediately.
2021-06-13 22:32:36 +02:00
Romain Vimont
cda4aa1623 Add method to send a broadcast
Implement the equivalent of Context.sendBroadcast(Intent).
2021-06-13 22:32:36 +02:00
Romain Vimont
310f5721d9 Move file_handler initialization
This will allow to (properly) reference the controller from the
file_handler initializer.
2021-06-13 22:32:36 +02:00
25 changed files with 236 additions and 178 deletions

View File

@@ -303,8 +303,7 @@ To start scrcpy using a v4l2 sink:
```bash ```bash
scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN
scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window scrcpy --v4l2-sink=/dev/videoN -N # --no-display to disable mirroring window
scrcpy --v4l2-sink=/dev/videoN -N # short version
``` ```
(replace `N` by the device ID, check with `ls /dev/video*`) (replace `N` by the device ID, check with `ls /dev/video*`)
@@ -710,15 +709,15 @@ There is no visual feedback, a log is printed to the console.
#### Push file to device #### Push file to device
To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK) To push a file to `/sdcard/` on the device, drag & drop a (non-APK) file to the
file to the _scrcpy_ window. _scrcpy_ window.
There is no visual feedback, a log is printed to the console. There is no visual feedback, a log is printed to the console.
The target directory can be changed on start: The target directory can be changed on start:
```bash ```bash
scrcpy --push-target=/sdcard/Movies/ scrcpy --push-target=/sdcard/Download/
``` ```

View File

@@ -133,7 +133,7 @@ but breaks the expected behavior of alpha keys in games (typically WASD).
.BI "\-\-push\-target " path .BI "\-\-push\-target " path
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push". Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
Default is "/sdcard/Download/". Default is "/sdcard/".
.TP .TP
.BI "\-r, \-\-record " file .BI "\-r, \-\-record " file

View File

@@ -81,20 +81,14 @@ show_adb_installation_msg() {
static void static void
show_adb_err_msg(enum process_result err, const char *const argv[]) { show_adb_err_msg(enum process_result err, const char *const argv[]) {
#define MAX_COMMAND_STRING_LEN 1024 char buf[512];
char *buf = malloc(MAX_COMMAND_STRING_LEN);
if (!buf) {
LOGE("Failed to execute (could not allocate error message)");
return;
}
switch (err) { switch (err) {
case PROCESS_ERROR_GENERIC: case PROCESS_ERROR_GENERIC:
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); argv_to_string(argv, buf, sizeof(buf));
LOGE("Failed to execute: %s", buf); LOGE("Failed to execute: %s", buf);
break; break;
case PROCESS_ERROR_MISSING_BINARY: case PROCESS_ERROR_MISSING_BINARY:
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN); argv_to_string(argv, buf, sizeof(buf));
LOGE("Command not found: %s", buf); LOGE("Command not found: %s", buf);
LOGE("(make 'adb' accessible from your PATH or define its full" LOGE("(make 'adb' accessible from your PATH or define its full"
"path in the ADB environment variable)"); "path in the ADB environment variable)");
@@ -104,38 +98,29 @@ show_adb_err_msg(enum process_result err, const char *const argv[]) {
// do nothing // do nothing
break; break;
} }
free(buf);
} }
process_t process_t
adb_execute(const char *serial, const char *const adb_cmd[], size_t len) { adb_execute(const char *serial, const char *const adb_cmd[], size_t len) {
const char *cmd[len + 4];
int i; int i;
process_t process; process_t process;
cmd[0] = get_adb_command();
const char **argv = malloc((len + 4) * sizeof(*argv));
if (!argv) {
return PROCESS_NONE;
}
argv[0] = get_adb_command();
if (serial) { if (serial) {
argv[1] = "-s"; cmd[1] = "-s";
argv[2] = serial; cmd[2] = serial;
i = 3; i = 3;
} else { } else {
i = 1; i = 1;
} }
memcpy(&argv[i], adb_cmd, len * sizeof(const char *)); memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
argv[len + i] = NULL; cmd[len + i] = NULL;
enum process_result r = process_execute(argv, &process); enum process_result r = process_execute(cmd, &process);
if (r != PROCESS_SUCCESS) { if (r != PROCESS_SUCCESS) {
show_adb_err_msg(r, argv); show_adb_err_msg(r, cmd);
process = PROCESS_NONE; return PROCESS_NONE;
} }
free(argv);
return process; return process;
} }

View File

@@ -129,7 +129,7 @@ scrcpy_print_usage(const char *arg0) {
" --push-target path\n" " --push-target path\n"
" Set the target directory for pushing files to the device by\n" " Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n" " drag & drop. It is passed as-is to \"adb push\".\n"
" Default is \"/sdcard/Download/\".\n" " Default is \"/sdcard/\".\n"
"\n" "\n"
" -r, --record file.mp4\n" " -r, --record file.mp4\n"
" Record screen to file.\n" " Record screen to file.\n"

View File

@@ -80,6 +80,13 @@ control_msg_serialize(const struct control_msg *msg, unsigned char *buf) {
case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: case CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
buf[1] = msg->set_screen_power_mode.mode; buf[1] = msg->set_screen_power_mode.mode;
return 2; return 2;
case CONTROL_MSG_TYPE_SCAN_MEDIA:
{
size_t len = write_string(msg->scan_media.path,
CONTROL_MSG_SCAN_MEDIA_PATH_MAX_LENGTH,
&buf[1]);
return 1 + len;
}
case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
case CONTROL_MSG_TYPE_COLLAPSE_PANELS: case CONTROL_MSG_TYPE_COLLAPSE_PANELS:
@@ -102,6 +109,9 @@ control_msg_destroy(struct control_msg *msg) {
case CONTROL_MSG_TYPE_SET_CLIPBOARD: case CONTROL_MSG_TYPE_SET_CLIPBOARD:
free(msg->set_clipboard.text); free(msg->set_clipboard.text);
break; break;
case CONTROL_MSG_TYPE_SCAN_MEDIA:
free(msg->scan_media.path);
break;
default: default:
// do nothing // do nothing
break; break;

View File

@@ -17,6 +17,8 @@
// type: 1 byte; paste flag: 1 byte; length: 4 bytes // type: 1 byte; paste flag: 1 byte; length: 4 bytes
#define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6) #define CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (CONTROL_MSG_MAX_SIZE - 6)
#define CONTROL_MSG_SCAN_MEDIA_PATH_MAX_LENGTH 256
#define POINTER_ID_MOUSE UINT64_C(-1); #define POINTER_ID_MOUSE UINT64_C(-1);
#define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2); #define POINTER_ID_VIRTUAL_FINGER UINT64_C(-2);
@@ -33,6 +35,7 @@ enum control_msg_type {
CONTROL_MSG_TYPE_SET_CLIPBOARD, CONTROL_MSG_TYPE_SET_CLIPBOARD,
CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
CONTROL_MSG_TYPE_ROTATE_DEVICE, CONTROL_MSG_TYPE_ROTATE_DEVICE,
CONTROL_MSG_TYPE_SCAN_MEDIA,
}; };
enum screen_power_mode { enum screen_power_mode {
@@ -76,6 +79,9 @@ struct control_msg {
struct { struct {
enum screen_power_mode mode; enum screen_power_mode mode;
} set_screen_power_mode; } set_screen_power_mode;
struct {
char *path; // owned, to be freed by free()
} scan_media;
}; };
}; };

View File

@@ -4,9 +4,11 @@
#include <string.h> #include <string.h>
#include "adb.h" #include "adb.h"
#include "control_msg.h"
#include "controller.h"
#include "util/log.h" #include "util/log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/Download/" #define DEFAULT_PUSH_TARGET "/sdcard/"
static void static void
file_handler_request_destroy(struct file_handler_request *req) { file_handler_request_destroy(struct file_handler_request *req) {
@@ -14,7 +16,8 @@ file_handler_request_destroy(struct file_handler_request *req) {
} }
bool bool
file_handler_init(struct file_handler *file_handler, const char *serial, file_handler_init(struct file_handler *file_handler,
struct controller *controller, const char *serial,
const char *push_target) { const char *push_target) {
cbuf_init(&file_handler->queue); cbuf_init(&file_handler->queue);
@@ -50,6 +53,8 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET; file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
file_handler->controller = controller;
return true; return true;
} }
@@ -103,6 +108,24 @@ file_handler_request(struct file_handler *file_handler,
return res; return res;
} }
static bool
request_scan_media(struct file_handler *file_handler) {
struct control_msg msg;
msg.type = CONTROL_MSG_TYPE_SCAN_MEDIA;
msg.scan_media.path = strdup(file_handler->push_target);
if (!msg.scan_media.path) {
LOGW("Could not strdup() media path");
return false;
}
if (!controller_push_msg(file_handler->controller, &msg)) {
LOGW("Could not request 'scan media'");
return false;
}
return true;
}
static int static int
run_file_handler(void *data) { run_file_handler(void *data) {
struct file_handler *file_handler = data; struct file_handler *file_handler = data;
@@ -145,6 +168,7 @@ run_file_handler(void *data) {
if (process_check_success(process, "adb push", false)) { if (process_check_success(process, "adb push", false)) {
LOGI("%s successfully pushed to %s", req.file, LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target); file_handler->push_target);
request_scan_media(file_handler);
} else { } else {
LOGE("Failed to push %s to %s", req.file, LOGE("Failed to push %s to %s", req.file,
file_handler->push_target); file_handler->push_target);

View File

@@ -22,6 +22,7 @@ struct file_handler_request {
struct file_handler_request_queue CBUF(struct file_handler_request, 16); struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler { struct file_handler {
struct controller *controller;
char *serial; char *serial;
const char *push_target; const char *push_target;
sc_thread thread; sc_thread thread;
@@ -34,7 +35,8 @@ struct file_handler {
}; };
bool bool
file_handler_init(struct file_handler *file_handler, const char *serial, file_handler_init(struct file_handler *file_handler,
struct controller *controller, const char *serial,
const char *push_target); const char *push_target);
void void

View File

@@ -35,14 +35,11 @@ record_packet_new(const AVPacket *packet) {
return NULL; return NULL;
} }
rec->packet = av_packet_alloc(); // av_packet_ref() does not initialize all fields in old FFmpeg versions
if (!rec->packet) { // See <https://github.com/Genymobile/scrcpy/issues/707>
free(rec); av_init_packet(&rec->packet);
return NULL;
}
if (av_packet_ref(rec->packet, packet)) { if (av_packet_ref(&rec->packet, packet)) {
av_packet_free(&rec->packet);
free(rec); free(rec);
return NULL; return NULL;
} }
@@ -51,8 +48,7 @@ record_packet_new(const AVPacket *packet) {
static void static void
record_packet_delete(struct record_packet *rec) { record_packet_delete(struct record_packet *rec) {
av_packet_unref(rec->packet); av_packet_unref(&rec->packet);
av_packet_free(&rec->packet);
free(rec); free(rec);
} }
@@ -148,8 +144,8 @@ run_recorder(void *data) {
struct record_packet *last = recorder->previous; struct record_packet *last = recorder->previous;
if (last) { if (last) {
// assign an arbitrary duration to the last packet // assign an arbitrary duration to the last packet
last->packet->duration = 100000; last->packet.duration = 100000;
bool ok = recorder_write(recorder, last->packet); bool ok = recorder_write(recorder, &last->packet);
if (!ok) { if (!ok) {
// failing to write the last frame is not very serious, no // failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file // future frame may depend on it, so the resulting file
@@ -176,14 +172,13 @@ run_recorder(void *data) {
} }
// config packets have no PTS, we must ignore them // config packets have no PTS, we must ignore them
if (rec->packet->pts != AV_NOPTS_VALUE if (rec->packet.pts != AV_NOPTS_VALUE
&& previous->packet->pts != AV_NOPTS_VALUE) { && previous->packet.pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet // we now know the duration of the previous packet
previous->packet->duration = previous->packet.duration = rec->packet.pts - previous->packet.pts;
rec->packet->pts - previous->packet->pts;
} }
bool ok = recorder_write(recorder, previous->packet); bool ok = recorder_write(recorder, &previous->packet);
record_packet_delete(previous); record_packet_delete(previous);
if (!ok) { if (!ok) {
LOGE("Could not record packet"); LOGE("Could not record packet");

View File

@@ -13,7 +13,7 @@
#include "util/thread.h" #include "util/thread.h"
struct record_packet { struct record_packet {
AVPacket *packet; AVPacket packet;
struct record_packet *next; struct record_packet *next;
}; };

View File

@@ -297,14 +297,6 @@ scrcpy(const struct scrcpy_options *options) {
goto end; goto end;
} }
if (options->display && options->control) {
if (!file_handler_init(&s->file_handler, s->server.serial,
options->push_target)) {
goto end;
}
file_handler_initialized = true;
}
struct decoder *dec = NULL; struct decoder *dec = NULL;
bool needs_decoder = options->display; bool needs_decoder = options->display;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
@@ -353,6 +345,12 @@ scrcpy(const struct scrcpy_options *options) {
goto end; goto end;
} }
controller_started = true; controller_started = true;
if (!file_handler_init(&s->file_handler, &s->controller,
s->server.serial, options->push_target)) {
goto end;
}
file_handler_initialized = true;
} }
const char *window_title = const char *window_title =

View File

@@ -41,18 +41,6 @@ get_window_size(const struct screen *screen) {
return size; return size;
} }
static struct point
get_window_position(const struct screen *screen) {
int x;
int y;
SDL_GetWindowPosition(screen->window, &x, &y);
struct point point;
point.x = x;
point.y = y;
return point;
}
// set the window size to be applied when fullscreen is disabled // set the window size to be applied when fullscreen is disabled
static void static void
set_window_size(struct screen *screen, struct size new_size) { set_window_size(struct screen *screen, struct size new_size) {
@@ -134,6 +122,13 @@ get_optimal_size(struct size current_size, struct size content_size) {
return window_size; return window_size;
} }
// same as get_optimal_size(), but read the current size from the window
static inline struct size
get_optimal_window_size(const struct screen *screen, struct size content_size) {
struct size window_size = get_window_size(screen);
return get_optimal_size(window_size, content_size);
}
// initially, there is no current size, so use the frame size as current size // initially, there is no current size, so use the frame size as current size
// req_width and req_height, if not 0, are the sizes requested by the user // req_width and req_height, if not 0, are the sizes requested by the user
static inline struct size static inline struct size
@@ -667,20 +662,9 @@ screen_resize_to_fit(struct screen *screen) {
return; return;
} }
struct point point = get_window_position(screen);
struct size window_size = get_window_size(screen);
struct size optimal_size = struct size optimal_size =
get_optimal_size(window_size, screen->content_size); get_optimal_window_size(screen, screen->content_size);
// Center the window related to the device screen
assert(optimal_size.width <= window_size.width);
assert(optimal_size.height <= window_size.height);
uint32_t new_x = point.x + (window_size.width - optimal_size.width) / 2;
uint32_t new_y = point.y + (window_size.height - optimal_size.height) / 2;
SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height); SDL_SetWindowSize(screen->window, optimal_size.width, optimal_size.height);
SDL_SetWindowPosition(screen->window, new_x, new_y);
LOGD("Resized to optimal size: %ux%u", optimal_size.width, LOGD("Resized to optimal size: %ux%u", optimal_size.width,
optimal_size.height); optimal_size.height);
} }
@@ -742,7 +726,6 @@ screen_handle_event(struct screen *screen, SDL_Event *event) {
} }
screen->maximized = false; screen->maximized = false;
apply_pending_resize(screen); apply_pending_resize(screen);
screen_render(screen, true);
break; break;
} }
return true; return true;

View File

@@ -104,38 +104,33 @@ static bool
stream_push_packet(struct stream *stream, AVPacket *packet) { stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE; bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immediately (it contains no // A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet. // frame); instead, it must be concatenated with the future data packet.
if (stream->pending || is_config) { if (stream->has_pending || is_config) {
size_t offset; size_t offset;
if (stream->pending) { if (stream->has_pending) {
offset = stream->pending->size; offset = stream->pending.size;
if (av_grow_packet(stream->pending, packet->size)) { if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet"); LOGE("Could not grow packet");
return false; return false;
} }
} else { } else {
offset = 0; offset = 0;
stream->pending = av_packet_alloc(); if (av_new_packet(&stream->pending, packet->size)) {
if (!stream->pending) {
LOGE("Could not allocate packet");
return false;
}
if (av_new_packet(stream->pending, packet->size)) {
LOGE("Could not create packet"); LOGE("Could not create packet");
av_packet_free(&stream->pending);
return false; return false;
} }
stream->has_pending = true;
} }
memcpy(stream->pending->data + offset, packet->data, packet->size); memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) { if (!is_config) {
// prepare the concat packet to send to the decoder // prepare the concat packet to send to the decoder
stream->pending->pts = packet->pts; stream->pending.pts = packet->pts;
stream->pending->dts = packet->dts; stream->pending.dts = packet->dts;
stream->pending->flags = packet->flags; stream->pending.flags = packet->flags;
packet = stream->pending; packet = &stream->pending;
} }
} }
@@ -149,10 +144,10 @@ stream_push_packet(struct stream *stream, AVPacket *packet) {
// data packet // data packet
bool ok = stream_parse(stream, packet); bool ok = stream_parse(stream, packet);
if (stream->pending) { if (stream->has_pending) {
// the pending packet must be discarded (consumed or error) // the pending packet must be discarded (consumed or error)
av_packet_unref(stream->pending); stream->has_pending = false;
av_packet_free(&stream->pending); av_packet_unref(&stream->pending);
} }
if (!ok) { if (!ok) {
@@ -220,21 +215,16 @@ run_stream(void *data) {
// It's more complicated, but this allows to reduce the latency by 1 frame! // It's more complicated, but this allows to reduce the latency by 1 frame!
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES; stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOGE("Could not allocate packet");
goto finally_close_parser;
}
for (;;) { for (;;) {
bool ok = stream_recv_packet(stream, packet); AVPacket packet;
bool ok = stream_recv_packet(stream, &packet);
if (!ok) { if (!ok) {
// end of stream // end of stream
break; break;
} }
ok = stream_push_packet(stream, packet); ok = stream_push_packet(stream, &packet);
av_packet_unref(packet); av_packet_unref(&packet);
if (!ok) { if (!ok) {
// cannot process packet (error already logged) // cannot process packet (error already logged)
break; break;
@@ -243,13 +233,10 @@ run_stream(void *data) {
LOGD("End of frames"); LOGD("End of frames");
if (stream->pending) { if (stream->has_pending) {
av_packet_unref(stream->pending); av_packet_unref(&stream->pending);
av_packet_free(&stream->pending);
} }
av_packet_free(&packet);
finally_close_parser:
av_parser_close(stream->parser); av_parser_close(stream->parser);
finally_close_sinks: finally_close_sinks:
stream_close_sinks(stream); stream_close_sinks(stream);
@@ -265,7 +252,7 @@ void
stream_init(struct stream *stream, socket_t socket, stream_init(struct stream *stream, socket_t socket,
const struct stream_callbacks *cbs, void *cbs_userdata) { const struct stream_callbacks *cbs, void *cbs_userdata) {
stream->socket = socket; stream->socket = socket;
stream->pending = NULL; stream->has_pending = false;
stream->sink_count = 0; stream->sink_count = 0;
assert(cbs && cbs->on_eos); assert(cbs && cbs->on_eos);

View File

@@ -24,7 +24,8 @@ struct stream {
AVCodecParserContext *parser; AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config // successive packets may need to be concatenated, until a non-config
// packet is available // packet is available
AVPacket *pending; bool has_pending;
AVPacket pending;
const struct stream_callbacks *cbs; const struct stream_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;

View File

@@ -6,9 +6,7 @@
#include "util/log.h" #include "util/log.h"
#include "util/str_util.h" #include "util/str_util.h"
#define CMD_MAX_LEN 8192 static int
static bool
build_cmd(char *cmd, size_t len, const char *const argv[]) { build_cmd(char *cmd, size_t len, const char *const argv[]) {
// Windows command-line parsing is WTF: // Windows command-line parsing is WTF:
// <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS> // <http://daviddeley.com/autohotkey/parameters/parameters.htm#WINPASS>
@@ -17,9 +15,9 @@ build_cmd(char *cmd, size_t len, const char *const argv[]) {
size_t ret = xstrjoin(cmd, argv, ' ', len); size_t ret = xstrjoin(cmd, argv, ' ', len);
if (ret >= len) { if (ret >= len) {
LOGE("Command too long (%" PRIsizet " chars)", len - 1); LOGE("Command too long (%" PRIsizet " chars)", len - 1);
return false; return -1;
} }
return true; return 0;
} }
enum process_result enum process_result
@@ -29,14 +27,13 @@ process_execute(const char *const argv[], HANDLE *handle) {
memset(&si, 0, sizeof(si)); memset(&si, 0, sizeof(si));
si.cb = sizeof(si); si.cb = sizeof(si);
char *cmd = malloc(CMD_MAX_LEN); char cmd[256];
if (!cmd || !build_cmd(cmd, CMD_MAX_LEN, argv)) { if (build_cmd(cmd, sizeof(cmd), argv)) {
*handle = NULL; *handle = NULL;
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;
} }
wchar_t *wide = utf8_to_wide_char(cmd); wchar_t *wide = utf8_to_wide_char(cmd);
free(cmd);
if (!wide) { if (!wide) {
LOGC("Could not allocate wide char string"); LOGC("Could not allocate wide char string");
return PROCESS_ERROR_GENERIC; return PROCESS_ERROR_GENERIC;

View File

@@ -86,7 +86,7 @@ encode_and_write_frame(struct sc_v4l2_sink *vs, const AVFrame *frame) {
return false; return false;
} }
AVPacket *packet = vs->packet; AVPacket *packet = &vs->packet;
ret = avcodec_receive_packet(vs->encoder_ctx, packet); ret = avcodec_receive_packet(vs->encoder_ctx, packet);
if (ret == 0) { if (ret == 0) {
// A packet was received // A packet was received
@@ -235,17 +235,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
goto error_avcodec_close; goto error_avcodec_close;
} }
vs->packet = av_packet_alloc();
if (!vs->packet) {
LOGE("Could not allocate packet");
goto error_av_frame_free;
}
LOGD("Starting v4l2 thread"); LOGD("Starting v4l2 thread");
ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs); ok = sc_thread_create(&vs->thread, run_v4l2_sink, "v4l2", vs);
if (!ok) { if (!ok) {
LOGC("Could not start v4l2 thread"); LOGC("Could not start v4l2 thread");
goto error_av_packet_free; goto error_av_frame_free;
} }
vs->header_written = false; vs->header_written = false;
@@ -255,8 +249,6 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
return true; return true;
error_av_packet_free:
av_packet_free(&vs->packet);
error_av_frame_free: error_av_frame_free:
av_frame_free(&vs->frame); av_frame_free(&vs->frame);
error_avcodec_close: error_avcodec_close:
@@ -286,7 +278,6 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
sc_thread_join(&vs->thread, NULL); sc_thread_join(&vs->thread, NULL);
av_packet_free(&vs->packet);
av_frame_free(&vs->frame); av_frame_free(&vs->frame);
avcodec_close(vs->encoder_ctx); avcodec_close(vs->encoder_ctx);
avcodec_free_context(&vs->encoder_ctx); avcodec_free_context(&vs->encoder_ctx);

View File

@@ -26,7 +26,7 @@ struct sc_v4l2_sink {
bool header_written; bool header_written;
AVFrame *frame; AVFrame *frame;
AVPacket *packet; AVPacket packet;
}; };
bool bool

View File

@@ -278,6 +278,28 @@ static void test_serialize_rotate_device(void) {
assert(!memcmp(buf, expected, sizeof(expected))); assert(!memcmp(buf, expected, sizeof(expected)));
} }
static void test_serialize_scan_media(void) {
struct control_msg msg = {
.type = CONTROL_MSG_TYPE_SCAN_MEDIA,
.scan_media = {
.path = "/sdcard/Download/",
},
};
unsigned char buf[CONTROL_MSG_MAX_SIZE];
size_t size = control_msg_serialize(&msg, buf);
assert(size == 22);
const unsigned char expected[] = {
CONTROL_MSG_TYPE_SCAN_MEDIA,
0x00, 0x00, 0x00, 0x11, // path length
'/', 's', 'd', 'c', 'a', 'r', 'd', '/',
'D', 'o', 'w', 'n', 'l', 'o', 'a', 'd',
'/' // path
};
assert(!memcmp(buf, expected, sizeof(expected)));
}
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
(void) argc; (void) argc;
(void) argv; (void) argv;
@@ -295,5 +317,6 @@ int main(int argc, char *argv[]) {
test_serialize_set_clipboard(); test_serialize_set_clipboard();
test_serialize_set_screen_power_mode(); test_serialize_set_screen_power_mode();
test_serialize_rotate_device(); test_serialize_rotate_device();
test_serialize_scan_media();
return 0; return 0;
} }

View File

@@ -17,6 +17,7 @@ public final class ControlMessage {
public static final int TYPE_SET_CLIPBOARD = 9; public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10; public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_ROTATE_DEVICE = 11;
public static final int TYPE_SCAN_MEDIA = 12;
private int type; private int type;
private String text; private String text;
@@ -97,6 +98,13 @@ public final class ControlMessage {
return msg; return msg;
} }
public static ControlMessage createScanMedia(String path) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SCAN_MEDIA;
msg.text = path;
return msg;
}
public static ControlMessage createEmpty(int type) { public static ControlMessage createEmpty(int type) {
ControlMessage msg = new ControlMessage(); ControlMessage msg = new ControlMessage();
msg.type = type; msg.type = type;

View File

@@ -76,6 +76,9 @@ public class ControlMessageReader {
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode(); msg = parseSetScreenPowerMode();
break; break;
case ControlMessage.TYPE_SCAN_MEDIA:
msg = parseScanMedia();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_COLLAPSE_PANELS:
@@ -182,6 +185,14 @@ public class ControlMessageReader {
return ControlMessage.createSetScreenPowerMode(mode); return ControlMessage.createSetScreenPowerMode(mode);
} }
private ControlMessage parseScanMedia() {
String path = parseString();
if (path == null) {
return null;
}
return ControlMessage.createScanMedia(path);
}
private static Position readPosition(ByteBuffer buffer) { private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt(); int x = buffer.getInt();
int y = buffer.getInt(); int y = buffer.getInt();

View File

@@ -1,5 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.content.Intent;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
@@ -7,6 +9,7 @@ import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@@ -135,6 +138,13 @@ public class Controller {
case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice(); Device.rotateDevice();
break; break;
case ControlMessage.TYPE_SCAN_MEDIA:
String path = msg.getText();
@SuppressWarnings("deprecation")
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(new File(path)));
Device.sendBroadcast(intent);
break;
default: default:
// do nothing // do nothing
} }

View File

@@ -8,6 +8,7 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager; import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener; import android.content.IOnPrimaryClipChangedListener;
import android.content.Intent;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
@@ -299,4 +300,8 @@ public final class Device {
public static ContentProvider createSettingsProvider() { public static ContentProvider createSettingsProvider() {
return SERVICE_MANAGER.getActivityManager().createSettingsProvider(); return SERVICE_MANAGER.getActivityManager().createSettingsProvider();
} }
public static void sendBroadcast(Intent intent) {
SERVICE_MANAGER.getActivityManager().sendBroadcast(intent);
}
} }

View File

@@ -2,7 +2,9 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.IInterface; import android.os.IInterface;
@@ -16,6 +18,7 @@ public class ActivityManager {
private Method getContentProviderExternalMethod; private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodNewVersion = true; private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod; private Method removeContentProviderExternalMethod;
private Method broadcastIntentMethod;
public ActivityManager(IInterface manager) { public ActivityManager(IInterface manager) {
this.manager = manager; this.manager = manager;
@@ -42,6 +45,22 @@ public class ActivityManager {
return removeContentProviderExternalMethod; return removeContentProviderExternalMethod;
} }
private Method getBroadcastIntentMethod() throws NoSuchMethodException {
if (broadcastIntentMethod == null) {
try {
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
Class<?> iIntentReceiverClass = Class.forName("android.content.IIntentReceiver");
broadcastIntentMethod = manager.getClass()
.getMethod("broadcastIntent", iApplicationThreadClass, Intent.class, String.class, iIntentReceiverClass, int.class,
String.class, Bundle.class, String[].class, int.class, Bundle.class, boolean.class, boolean.class, int.class);
return broadcastIntentMethod;
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
return broadcastIntentMethod;
}
private ContentProvider getContentProviderExternal(String name, IBinder token) { private ContentProvider getContentProviderExternal(String name, IBinder token) {
try { try {
Method method = getGetContentProviderExternalMethod(); Method method = getGetContentProviderExternalMethod();
@@ -84,4 +103,13 @@ public class ActivityManager {
public ContentProvider createSettingsProvider() { public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder()); return getContentProviderExternal("settings", new Binder());
} }
public void sendBroadcast(Intent intent) {
try {
Method method = getBroadcastIntentMethod();
method.invoke(manager, null, intent, null, null, 0, null, null, null, -1, null, true, false, ServiceManager.USER_ID);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
} }

View File

@@ -2,7 +2,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln; import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
@@ -38,8 +37,6 @@ public class ContentProvider implements Closeable {
private Method callMethod; private Method callMethod;
private int callMethodVersion; private int callMethodVersion;
private Object attributionSource;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) { ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager; this.manager = manager;
this.provider = provider; this.provider = provider;
@@ -47,58 +44,36 @@ public class ContentProvider implements Closeable {
this.token = token; this.token = token;
} }
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException { private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) { if (callMethod == null) {
try { try {
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource"); callMethod = provider.getClass()
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class); .getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0; callMethodVersion = 0;
} catch (NoSuchMethodException | ClassNotFoundException e0) { } catch (NoSuchMethodException e) {
// old versions // old versions
try { try {
callMethod = provider.getClass() callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
.getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 1; callMethodVersion = 1;
} catch (NoSuchMethodException e1) { } catch (NoSuchMethodException e2) {
try { callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 2;
callMethodVersion = 2;
} catch (NoSuchMethodException e2) {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethodVersion = 3;
}
} }
} }
} }
return callMethod; return callMethod;
} }
@SuppressLint("PrivateApi")
private Object getAttributionSource()
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (attributionSource == null) {
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
}
return attributionSource;
}
private Bundle call(String callMethod, String arg, Bundle extras) { private Bundle call(String callMethod, String arg, Bundle extras) {
try { try {
Method method = getCallMethod(); Method method = getCallMethod();
Object[] args; Object[] args;
switch (callMethodVersion) { switch (callMethodVersion) {
case 0: case 0:
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
break;
case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras}; args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break; break;
case 2: case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras}; args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break; break;
default: default:
@@ -106,7 +81,7 @@ public class ContentProvider implements Closeable {
break; break;
} }
return (Bundle) method.invoke(provider, args); return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) { } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e); Ln.e("Could not invoke method", e);
return null; return null;
} }

View File

@@ -314,6 +314,26 @@ public class ControlMessageReaderTest {
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType()); Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
} }
@Test
public void testScanMedia() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SCAN_MEDIA);
byte[] text = "/sdcard/Download/".getBytes(StandardCharsets.UTF_8);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SCAN_MEDIA, event.getType());
Assert.assertEquals("/sdcard/Download/", event.getText());
}
@Test @Test
public void testMultiEvents() throws IOException { public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader(); ControlMessageReader reader = new ControlMessageReader();