diff --git a/app/src/cli.c b/app/src/cli.c index b2e3e30a53..205e194fa9 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -621,14 +621,20 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NEW_DISPLAY, .longopt = "new-display", - .argdesc = "[x][/]", + .argdesc = "[x][/][:r[]]", .optional_arg = true, .text = "Create a new display with the specified resolution and " "density. If not provided, they default to the main display " - "dimensions and DPI.\n" + "dimensions and DPI. Add ':r' to make the display resizable. " + "Optionally add a resolution factor after ':r' to scale the " + "display size (between 0.1 and 10.0).\n" "Examples:\n" " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" + " --new-display=1920x1080/420:r # resizable display\n" + " --new-display=1920x1080/420:r2.0 # resizable with 2x factor\n" + " --new-display=:r0.5 # resizable with main display size and 0.5x factor\n" + " --new-display=/420:r1.5 # resizable with main display size, 420 dpi and 1.5x factor\n" " --new-display # main display size and density\n" " --new-display=/240 # main display size and 240 dpi", }, diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e46c61656d..c9d7439f0f 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -182,6 +182,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255); return 1 + len; } + case SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY: + sc_write16be(&buf[1], msg->resize_display.width); + sc_write16be(&buf[3], msg->resize_display.height); + return 5; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -318,6 +322,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_RESET_VIDEO: LOG_CMSG("reset video"); break; + case SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY: + LOG_CMSG("resize display to %dx%d", msg->resize_display.width, msg->resize_display.height); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 74dbcba800..0c216688ae 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -43,6 +43,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_START_APP, SC_CONTROL_MSG_TYPE_RESET_VIDEO, + SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY, }; enum sc_copy_key { @@ -111,6 +112,10 @@ struct sc_control_msg { struct { char *name; } start_app; + struct { + uint16_t width; + uint16_t height; + } resize_display; }; }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3e4dd0f32f..d6ab31ace0 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -541,6 +541,11 @@ sc_input_manager_process_key(struct sc_input_manager *im, if (shift) { reset_video(im); } else { + // Disable MOD+R rotation in resizable virtual display mode + if (im->screen->resizable_new_display) { + LOGI("MOD+R rotation disabled in resizable virtual display mode"); + return; + } rotate_device(im); } } diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index aedfdf9cf8..2823f71f0e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -802,6 +802,62 @@ scrcpy(struct scrcpy_options *options) { const char *window_title = options->window_title ? options->window_title : info->device_name; + // Check if new_display is resizable (contains :r) + bool resizable_new_display = false; + float resolution_factor = 1.0f; + bool auto_resolution_factor = false; + + if (options->new_display) { + const char *r_pos = strstr(options->new_display, ":r"); + if (r_pos) { + resizable_new_display = true; + // Check if there's a resolution factor after ":r" + const char *factor_start = r_pos + 2; // Skip ":r" + if (*factor_start && (*factor_start >= '0' && *factor_start <= '9')) { + // Parse the resolution factor + char *end; + resolution_factor = strtof(factor_start, &end); + if (resolution_factor <= 0.1f) { + // Prevent too small factors + resolution_factor = 0.1f; + } else if (resolution_factor > 10.0f) { + // Prevent too large factors + resolution_factor = 10.0f; + } + } else { + // No explicit resolution factor provided, will calculate automatically + auto_resolution_factor = true; + LOGI("No resolution factor specified, will calculate automatically"); + } + } + } + + // If automatic resolution factor calculation was requested, calculate it based on the main display size + if (auto_resolution_factor && resizable_new_display) { + // Get the main display size + SDL_Rect display_bounds; + if (SDL_GetDisplayUsableBounds(0, &display_bounds) == 0) { + // Calculate the resolution factor based on the ratio between the display size and the requested window size + int display_width = display_bounds.w; + int display_height = display_bounds.h; + int window_width = options->window_width ? options->window_width : display_width / 2; + int window_height = options->window_height ? options->window_height : display_height / 2; + float width_factor = (float)display_width / window_width; + float height_factor = (float)display_height / window_height; + resolution_factor = width_factor < height_factor ? width_factor : height_factor; + if (resolution_factor < 0.1f) { + resolution_factor = 0.1f; + } else if (resolution_factor > 10.0f) { + resolution_factor = 10.0f; + } + LOGI("Calculated automatic resolution factor: %.2f (display: %dx%d, window: %dx%d)", + resolution_factor, display_width, display_height, window_width, window_height); + } else { + LOGW("Could not get display bounds, using default resolution factor: 1.0"); + resolution_factor = 1.0f; + } + } + struct sc_screen_params screen_params = { .video = options->video_playback, .controller = controller, @@ -824,6 +880,8 @@ scrcpy(struct scrcpy_options *options) { .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, + .resizable_new_display = resizable_new_display, + .resolution_factor = resolution_factor, }; if (!sc_screen_init(&s->screen, &screen_params)) { diff --git a/app/src/screen.c b/app/src/screen.c index da17df0ed2..c6dbaf86d2 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -11,8 +11,18 @@ #define DISPLAY_MARGINS 96 +// --- Fix: Ensure these are defined before any use --- +#define RESIZE_FINISHED_DELAY 200 +static Uint32 resize_timer_callback(Uint32 interval, void *param); + #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) +// Prototipo para evitar error de declaración implícita +static bool sizes_are_close(int a, int b, int tolerance); + +static void +sc_screen_send_resize_display(struct sc_screen *screen, int width, int height); + static inline struct sc_size get_oriented_size(struct sc_size size, enum sc_orientation orientation) { struct sc_size oriented_size; @@ -239,11 +249,9 @@ static int event_watcher(void *data, SDL_Event *event) { struct sc_screen *screen = data; assert(screen->video); - if (event->type == SDL_WINDOWEVENT && event->window.event == SDL_WINDOWEVENT_RESIZED) { - // In practice, it seems to always be called from the same thread in - // that specific case. Anyway, it's just a workaround. + // Only handle rendering for the workaround, resize logic should be handled elsewhere sc_screen_render(screen, true); } return 0; @@ -333,6 +341,9 @@ sc_screen_init(struct sc_screen *screen, screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; + screen->initial_setup = true; + screen->content_driven_resize = false; + screen->last_resize_time = 0; screen->video = params->video; @@ -343,6 +354,11 @@ sc_screen_init(struct sc_screen *screen, screen->req.fullscreen = params->fullscreen; screen->req.start_fps_counter = params->start_fps_counter; + screen->resizable_new_display = params->resizable_new_display; + screen->resolution_factor = params->resolution_factor; + + LOGI("Resolution factor: %.2f", screen->resolution_factor); + bool ok = sc_frame_buffer_init(&screen->fb); if (!ok) { return false; @@ -508,6 +524,10 @@ sc_screen_show_initial_window(struct sc_screen *screen) { SDL_ShowWindow(screen->window); sc_screen_update_content_rect(screen); + + // Mark initial setup as complete - now user resizing will trigger display resize + LOGD("Initial setup complete, enabling user resize detection"); + screen->initial_setup = false; } void @@ -537,11 +557,20 @@ sc_screen_destroy(struct sc_screen *screen) { sc_frame_buffer_destroy(&screen->fb); } +// Utilidad para comparar tamaños con tolerancia +static bool sizes_are_close(int a, int b, int tolerance) { + return abs(a - b) <= tolerance; +} + static void resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, struct sc_size new_content_size) { assert(screen->video); - + if (screen->resizable_new_display) { + // En modo redimensionable, el display nunca debe modificar la ventana + // Por lo tanto, ignorar cualquier resize proveniente del display + return; + } struct sc_size window_size = get_window_size(screen); struct sc_size target_size = { .width = (uint32_t) window_size.width * new_content_size.width @@ -550,7 +579,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size, / old_content_size.height, }; target_size = get_optimal_size(target_size, new_content_size, true); + + // Mark that we're doing a content-driven resize to avoid feedback loop + screen->content_driven_resize = true; set_window_size(screen, target_size); + screen->content_driven_resize = false; } static void @@ -577,8 +610,11 @@ apply_pending_resize(struct sc_screen *screen) { assert(!screen->maximized); assert(!screen->minimized); if (screen->resize_pending) { + // Mark that we're doing a content-driven resize to avoid feedback loop + screen->content_driven_resize = true; resize_for_content(screen, screen->windowed_content_size, screen->content_size); + screen->content_driven_resize = false; screen->resize_pending = false; } } @@ -800,6 +836,17 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { + // --- Manejo especial para SDL_USEREVENT de resize diferido --- + if (event->type == SDL_USEREVENT && event->user.code == 0x5343525A && event->user.data1 == screen) { + // Timer expirado: notificar resize + if (screen->pending_resize_width > 0 && screen->pending_resize_height > 0) { + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d", screen->pending_resize_width, screen->pending_resize_height); + sc_screen_send_resize_display(screen, screen->pending_resize_width, screen->pending_resize_height); + } + screen->resize_timer = 0; + return true; + } + // --- Fin manejo especial --- switch (event->type) { case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size @@ -835,6 +882,37 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_SIZE_CHANGED: + if (screen->resizable_new_display) { + int w, h; + SDL_GetWindowSize(screen->window, &w, &h); + if ((!sizes_are_close(w, screen->last_window_width, 2) || !sizes_are_close(h, screen->last_window_height, 2)) && + !screen->initial_setup && !screen->content_driven_resize) { + screen->last_window_width = w; + screen->last_window_height = h; + // --- Resize diferido --- + // Round to multiple of 8 to match server-side rounding and avoid quality degradation + int rounded_w = (w + 4) & ~7; // Round to nearest multiple of 8 + int rounded_h = (h + 4) & ~7; // Round to nearest multiple of 8 + + // Resize window to rounded dimensions to maintain quality + if (rounded_w != w || rounded_h != h) { + SDL_SetWindowSize(screen->window, rounded_w, rounded_h); + LOGD("Window resized to rounded dimensions: %dx%d (from %dx%d)", rounded_w, rounded_h, w, h); + } + + screen->pending_resize_width = rounded_w; + screen->pending_resize_height = rounded_h; + if (screen->resize_timer) { + SDL_RemoveTimer(screen->resize_timer); + } + screen->resize_timer = SDL_AddTimer(RESIZE_FINISHED_DELAY, resize_timer_callback, screen); + // --- Fin resize diferido --- + } else if (screen->initial_setup) { + LOGD("[SIZE_CHANGED] Initial setup resize ignored: %dx%d", w, h); + } else if (screen->content_driven_resize) { + LOGD("[SIZE_CHANGED] Content-driven resize ignored: %dx%d", w, h); + } + } sc_screen_render(screen, true); break; case SDL_WINDOWEVENT_MAXIMIZED: @@ -871,9 +949,63 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { return true; } +static void +sc_screen_send_resize_display(struct sc_screen *screen, int width, int height) { + if (!screen->im.controller) { + return; + } + + // Validate minimum size constraints to avoid Android errors + if (width < 1 || height < 1) { + LOGD("Display size too small, ignoring resize: %dx%d", width, height); + return; + } + + // Apply resolution factor if in resizable mode + int adjusted_width = width; + int adjusted_height = height; + + if (screen->resizable_new_display && screen->resolution_factor != 1.0f) { + adjusted_width = (int)(width * screen->resolution_factor); + adjusted_height = (int)(height * screen->resolution_factor); + + // Ensure minimum size + if (adjusted_width < 1) adjusted_width = 1; + if (adjusted_height < 1) adjusted_height = 1; + + // Ensure maximum size to avoid encoder issues + // Most Android devices support up to 4K resolution for encoding + const int MAX_DIMENSION = 2048; // Safe limit for most devices + if (adjusted_width > MAX_DIMENSION || adjusted_height > MAX_DIMENSION) { + // Scale down proportionally to fit within limits + float scale = 1.0f; + if (adjusted_width > adjusted_height) { + scale = (float)MAX_DIMENSION / adjusted_width; + } else { + scale = (float)MAX_DIMENSION / adjusted_height; + } + adjusted_width = (int)(adjusted_width * scale); + adjusted_height = (int)(adjusted_height * scale); + LOGW("Limiting dimensions to %dx%d to avoid encoder issues", adjusted_width, adjusted_height); + } + + LOGD("Applying resolution factor %.2f: %dx%d -> %dx%d", + screen->resolution_factor, width, height, adjusted_width, adjusted_height); + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_RESIZE_DISPLAY; + msg.resize_display.width = (uint16_t) adjusted_width; + msg.resize_display.height = (uint16_t) adjusted_height; + + if (!sc_controller_push_msg(screen->im.controller, &msg)) { + LOGW("Could not request display resize to %dx%d", adjusted_width, adjusted_height); + } +} + struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, - int32_t x, int32_t y) { + int32_t x, int32_t y) { assert(screen->video); enum sc_orientation orientation = screen->orientation; @@ -945,3 +1077,26 @@ sc_screen_hidpi_scale_coords(struct sc_screen *screen, int32_t *x, int32_t *y) { *x = (int64_t) *x * dw / ww; *y = (int64_t) *y * dh / wh; } + +// --- Resize diferido --- +static Uint32 resize_timer_callback(Uint32 interval, void *param) { + (void)interval; + struct sc_screen *screen = param; + assert(screen->video); + int width = screen->pending_resize_width; + int height = screen->pending_resize_height; + screen->pending_resize_width = 0; + screen->pending_resize_height = 0; + // Ignorar tamaños inválidos + if (width < 1 || height < 1) { + LOGD("Ignoring invalid resize: %dx%d", width, height); + return 0; + } + // Round to multiple of 8 to match server-side rounding and avoid quality degradation + int rounded_w = (width + 4) & ~7; // Round to nearest multiple of 8 + int rounded_h = (height + 4) & ~7; // Round to nearest multiple of 8 + + LOGD("[RESIZE_FINISHED] Notifying display resize: %dx%d (rounded from %dx%d)", rounded_w, rounded_h, width, height); + sc_screen_send_resize_display(screen, rounded_w, rounded_h); + return 0; +} diff --git a/app/src/screen.h b/app/src/screen.h index 6621b2d2d1..c9a58309bd 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -59,7 +59,21 @@ struct sc_screen { // client orientation enum sc_orientation orientation; // rectangle of the content (excluding black borders) - struct SDL_Rect rect; + SDL_Rect rect; + + // --- Añadido para evitar bucles y zonas negras en displays redimensionables --- + int last_window_width; + int last_window_height; + int last_display_width; + int last_display_height; + // ----------------------------------------------------------------------------- + + // --- Añadido para resize diferido --- + int pending_resize_width; + int pending_resize_height; + SDL_TimerID resize_timer; + // ----------------------------------- + bool has_frame; bool fullscreen; bool maximized; @@ -69,6 +83,12 @@ struct sc_screen { bool paused; AVFrame *resume_frame; + + bool resizable_new_display; + float resolution_factor; // Factor to multiply window size in resizable mode + bool initial_setup; // track if we're in initial window setup phase + bool content_driven_resize; // track if we're in content-driven resize (to avoid feedback loop) + uint64_t last_resize_time; // timestamp of last resize to implement debouncing }; struct sc_screen_params { @@ -100,6 +120,8 @@ struct sc_screen_params { bool fullscreen; bool start_fps_counter; + bool resizable_new_display; + float resolution_factor; // Factor to multiply window size in resizable mode }; // initialize screen, create window, renderer and texture (window is hidden) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 9f962127fd..1e2f7bb00e 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -11,6 +11,17 @@ scrcpy --new-display # use the main display size and density scrcpy --new-display=/240 # use the main display size and 240 dpi ``` +You can make the display resizable by adding `:r` to the option. Additionally, you can specify a resolution factor after `:r` to scale the display size: + +```bash +scrcpy --new-display=1920x1080:r # resizable display +scrcpy --new-display=1920x1080:r2.0 # resizable with 2x factor +scrcpy --new-display=:r0.5 # resizable with 0.5x factor +scrcpy --new-display=/420:r1.5 # resizable with 1.5x factor +``` + +The resolution factor must be between 0.1 and 10.0. + The new virtual display is destroyed on exit. ## Start app diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 66bb68e8a9..f30f520d2f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -1,5 +1,8 @@ package com.genymobile.scrcpy; +import android.graphics.Rect; +import android.util.Pair; + import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.device.Device; @@ -14,9 +17,6 @@ import com.genymobile.scrcpy.video.VideoSource; import com.genymobile.scrcpy.wrappers.WindowManager; -import android.graphics.Rect; -import android.util.Pair; - import java.util.List; import java.util.Locale; @@ -589,10 +589,26 @@ private static NewDisplay parseNewDisplay(String newDisplay) { // - "x/" // - "x" // - "/" + // - "x/:r" + // - "x:r" + // - "/:r" + // - ":r" + // - "x/:r" + // - "x:r" + // - "/:r" + // - ":r" if (newDisplay.isEmpty()) { return new NewDisplay(); } + // Check for resizable flag and resolution factor + boolean resizable = false; + int rIndex = newDisplay.indexOf(":r"); + if (rIndex >= 0) { + resizable = true; + newDisplay = newDisplay.substring(0, rIndex); + } + String[] tokens = newDisplay.split("/"); Size size; @@ -612,7 +628,7 @@ private static NewDisplay parseNewDisplay(String newDisplay) { dpi = 0; } - return new NewDisplay(size, dpi); + return new NewDisplay(size, dpi, resizable); } private static Pair parseCaptureOrientation(String value) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 0eb96adcbc..4c4e2b8f25 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -25,12 +25,12 @@ public final class ControlMessage { public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_START_APP = 16; public static final int TYPE_RESET_VIDEO = 17; + public static final int TYPE_RESIZE_DISPLAY = 18; public static final long SEQUENCE_INVALID = 0; public static final int COPY_KEY_NONE = 0; public static final int COPY_KEY_COPY = 1; - public static final int COPY_KEY_CUT = 2; private int type; private String text; @@ -53,6 +53,8 @@ public final class ControlMessage { private boolean on; private int vendorId; private int productId; + private int width; + private int height; private ControlMessage() { } @@ -166,6 +168,14 @@ public static ControlMessage createStartApp(String name) { return msg; } + public static ControlMessage createResizeDisplay(int width, int height) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_RESIZE_DISPLAY; + msg.width = width; + msg.height = height; + return msg; + } + public int getType() { return type; } @@ -249,4 +259,12 @@ public int getVendorId() { public int getProductId() { return productId; } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 830a7ec769..98cbc69092 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -48,6 +48,8 @@ public ControlMessage read() throws IOException { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: case ControlMessage.TYPE_RESET_VIDEO: return ControlMessage.createEmpty(type); + case ControlMessage.TYPE_RESIZE_DISPLAY: + return parseResizeDisplay(); case ControlMessage.TYPE_UHID_CREATE: return parseUhidCreate(); case ControlMessage.TYPE_UHID_INPUT: @@ -166,6 +168,12 @@ private ControlMessage parseStartApp() throws IOException { return ControlMessage.createStartApp(name); } + private ControlMessage parseResizeDisplay() throws IOException { + int width = dis.readUnsignedShort(); + int height = dis.readUnsignedShort(); + return ControlMessage.createResizeDisplay(width, height); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4a8e3ca84..b7da5b42b8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; @@ -312,6 +313,7 @@ private boolean handleEvent() throws IOException { break; case ControlMessage.TYPE_ROTATE_DEVICE: Device.rotateDevice(getActionDisplayId()); + // The virtual display resize will be handled by the main display monitor break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); @@ -331,6 +333,9 @@ private boolean handleEvent() throws IOException { case ControlMessage.TYPE_RESET_VIDEO: resetVideo(); break; + case ControlMessage.TYPE_RESIZE_DISPLAY: + resizeDisplay(msg.getWidth(), msg.getHeight()); + break; default: // do nothing } @@ -754,4 +759,18 @@ private void resetVideo() { surfaceCapture.requestInvalidate(); } } + + private void resizeDisplay(int width, int height) { + if (surfaceCapture instanceof NewDisplayCapture) { + NewDisplayCapture newDisplayCapture = (NewDisplayCapture) surfaceCapture; + if (newDisplayCapture.isResizable()) { + Ln.i("Resizing display to " + width + "x" + height); + newDisplayCapture.resizeDisplay(width, height); + } else { + Ln.w("Display is not resizable"); + } + } else { + Ln.w("Resize display not supported for current capture type"); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java index 3aa2996a18..c5270a05bf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -3,14 +3,17 @@ public final class NewDisplay { private Size size; private int dpi; + private final boolean resizable; public NewDisplay() { - // Auto size and dpi + // Auto size and dpi, not resizable + this.resizable = false; } - public NewDisplay(Size size, int dpi) { + public NewDisplay(Size size, int dpi, boolean resizable) { this.size = size; this.dpi = dpi; + this.resizable = resizable; } public Size getSize() { @@ -21,6 +24,10 @@ public int getDpi() { return dpi; } + public boolean isResizable() { + return resizable; + } + public boolean hasExplicitSize() { return size != null; } @@ -28,4 +35,5 @@ public boolean hasExplicitSize() { public boolean hasExplicitDpi() { return dpi != 0; } + } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 792b3a8a7a..c20fcf0453 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,7 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; - private int displayImePolicy; + private final int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -63,6 +63,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size physicalSize; // the physical size of the display (without rotation) private int dpi; + private final boolean resizable; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; @@ -78,6 +79,7 @@ public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.angle = options.getAngle(); this.vdDestroyContent = options.getVDDestroyContent(); this.vdSystemDecorations = options.getVDSystemDecorations(); + this.resizable = newDisplay.isResizable(); } @Override @@ -197,7 +199,11 @@ public void startNew(Surface surface) { ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); } - displaySizeMonitor.start(virtualDisplayId, this::invalidate); + // Only start display size monitoring for non-resizable displays + // For resizable displays, we control the resizing manually from the client + if (!resizable) { + displaySizeMonitor.start(virtualDisplayId, this::invalidate); + } } catch (Exception e) { Ln.e("Could not create display", e); throw new AssertionError("Could not create display"); @@ -264,4 +270,53 @@ private static int scaleDpi(Size initialSize, int initialDpi, Size size) { public void requestInvalidate() { invalidate(); } + + public void resizeDisplay(int newWidth, int newHeight) { + if (!resizable || virtualDisplay == null) { + return; + } + + // Validate minimum size constraints + if (newWidth < 1 || newHeight < 1) { + Ln.w("Display size too small, ignoring resize: " + newWidth + "x" + newHeight); + return; + } + + try { + // Calculate new DPI based on the size change + int newDpi = dpi; + if (displaySize != null) { + // Scale DPI proportionally to maintain similar pixel density + int oldMax = Math.max(displaySize.getWidth(), displaySize.getHeight()); + int newMax = Math.max(newWidth, newHeight); + newDpi = (dpi * newMax) / oldMax; + } + + // Ensure DPI is within valid range (Android requires DPI >= 1) + newDpi = Math.max(1, newDpi); + + // Round to multiple of 8 to avoid quality degradation from encoding constraints + Size roundedSize = new Size(newWidth, newHeight).round8(); + int roundedWidth = roundedSize.getWidth(); + int roundedHeight = roundedSize.getHeight(); + + // Resize the virtual display with rounded dimensions + virtualDisplay.resize(roundedWidth, roundedHeight, newDpi); + + // Update our internal state + displaySize = new Size(roundedWidth, roundedHeight); + dpi = newDpi; + + Ln.i("Resized display to: " + roundedWidth + "x" + roundedHeight + "/" + newDpi + " (rounded from " + newWidth + "x" + newHeight + ")"); + + // Trigger a reconfiguration + invalidate(); + } catch (Exception e) { + Ln.e("Could not resize display", e); + } + } + + public boolean isResizable() { + return resizable; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 236a5f4836..1aceb41dce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -199,7 +199,17 @@ private void encode(MediaCodec codec, Streamer streamer) throws IOException { boolean eos; do { - int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); + int outputBufferId; + try { + // Use a finite timeout to handle MediaCodec reset scenarios gracefully + outputBufferId = codec.dequeueOutputBuffer(bufferInfo, 10000); // 10ms timeout + } catch (IllegalStateException e) { + // This can happen when MediaCodec is being reset (e.g., during display resize) + // The pending dequeue request gets cancelled + Ln.d("MediaCodec dequeue interrupted during reset: " + e.getMessage()); + break; // Exit the loop gracefully + } + try { eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; // On EOS, there might be data or not, depending on bufferInfo.size