diff --git a/FAQ.md b/FAQ.md index d5f0e3ee5e..43ba39aff4 100644 --- a/FAQ.md +++ b/FAQ.md @@ -219,6 +219,9 @@ scrcpy -m 1024 scrcpy -m 800 ``` +Since scrcpy v1.22, scrcpy automatically tries again with a lower definition +before failing. This behavior can be disabled with `--no-downsize-on-error`. + You could also try another [encoder](README.md#encoder). diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 4d02982cbe..37431a91cb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -140,6 +140,12 @@ By default, scrcpy automatically synchronizes the computer clipboard to the devi This option disables this automatic synchronization. +.TP +.B \-\-no\-downsize\-on\-error +By default, on MediaCodec error, scrcpy automatically tries again with a lower definition. + +This option disables this behavior. + .TP .B \-n, \-\-no\-control Disable device control (mirror the device in read\-only). diff --git a/app/src/cli.c b/app/src/cli.c index 3fcf94eb62..69177aef90 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -52,6 +52,7 @@ #define OPT_NO_CLIPBOARD_AUTOSYNC 1032 #define OPT_TCPIP 1033 #define OPT_RAW_KEY_EVENTS 1034 +#define OPT_NO_DOWNSIZE_ON_ERROR 1035 struct sc_option { char shortopt; @@ -236,6 +237,13 @@ static const struct sc_option options[] = { "is preserved.\n" "Default is 0 (unlimited).", }, + { + .longopt_id = OPT_NO_DOWNSIZE_ON_ERROR, + .longopt = "no-downsize-on-error", + .text = "By default, on MediaCodec error, scrcpy automatically tries " + "again with a lower definition.\n" + "This option disables this behavior.", + }, { .longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC, .longopt = "no-clipboard-autosync", @@ -1489,6 +1497,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->tcpip = true; opts->tcpip_dst = optarg; break; + case OPT_NO_DOWNSIZE_ON_ERROR: + opts->downsize_on_error = false; + break; case OPT_V4L2_SINK: #ifdef HAVE_V4L2 opts->v4l2_device = optarg; @@ -1534,11 +1545,18 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->v4l2_device && opts->lock_video_orientation - == SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - LOGI("Video orientation is locked for v4l2 sink. " - "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + if (opts->v4l2_device) { + if (opts->lock_video_orientation == + SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { + LOGI("Video orientation is locked for v4l2 sink. " + "See --lock-video-orientation."); + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + } + + // V4L2 could not handle size change. + // Do not log because downsizing on error is the default behavior, + // not an explicit request from the user. + opts->downsize_on_error = false; } if (opts->v4l2_buffer && !opts->v4l2_device) { diff --git a/app/src/options.c b/app/src/options.c index 7a5b71af05..b8560406fc 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -54,6 +54,7 @@ const struct scrcpy_options scrcpy_options_default = { .legacy_paste = false, .power_off_on_close = false, .clipboard_autosync = true, + .downsize_on_error = true, .tcpip = false, .tcpip_dst = NULL, }; diff --git a/app/src/options.h b/app/src/options.h index b2c696644e..99f03d4053 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -129,6 +129,7 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; + bool downsize_on_error; bool tcpip; const char *tcpip_dst; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 86fdf98462..e55fef80ed 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -364,6 +364,7 @@ scrcpy(struct scrcpy_options *options) { .force_adb_forward = options->force_adb_forward, .power_off_on_close = options->power_off_on_close, .clipboard_autosync = options->clipboard_autosync, + .downsize_on_error = options->downsize_on_error, .tcpip = options->tcpip, .tcpip_dst = options->tcpip_dst, }; diff --git a/app/src/screen.c b/app/src/screen.c index a2796278e4..a357eecf3a 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -369,6 +369,12 @@ sc_screen_init(struct sc_screen *screen, screen->mouse_captured = false; screen->mouse_capture_key_pressed = 0; + screen->req.x = params->window_x; + screen->req.y = params->window_y; + screen->req.width = params->window_width; + screen->req.height = params->window_height; + screen->req.fullscreen = params->fullscreen; + static const struct sc_video_buffer_callbacks cbs = { .on_new_frame = sc_video_buffer_on_new_frame, }; @@ -397,9 +403,6 @@ sc_screen_init(struct sc_screen *screen, get_rotated_size(screen->frame_size, screen->rotation); screen->content_size = content_size; - struct sc_size window_size = - get_initial_optimal_size(content_size,params->window_width, - params->window_height); uint32_t window_flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; @@ -410,13 +413,9 @@ sc_screen_init(struct sc_screen *screen, window_flags |= SDL_WINDOW_BORDERLESS; } - int x = params->window_x != SC_WINDOW_POSITION_UNDEFINED - ? params->window_x : (int) SDL_WINDOWPOS_UNDEFINED; - int y = params->window_y != SC_WINDOW_POSITION_UNDEFINED - ? params->window_y : (int) SDL_WINDOWPOS_UNDEFINED; - screen->window = SDL_CreateWindow(params->window_title, x, y, - window_size.width, window_size.height, - window_flags); + // The window will be positioned and sized on first video frame + screen->window = + SDL_CreateWindow(params->window_title, 0, 0, 0, 0, window_flags); if (!screen->window) { LOGC("Could not create window: %s", SDL_GetError()); goto error_destroy_fps_counter; @@ -498,17 +497,6 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); - // Reset the window size to trigger a SIZE_CHANGED event, to workaround - // HiDPI issues with some SDL renderers when several displays having - // different HiDPI scaling are connected - SDL_SetWindowSize(screen->window, window_size.width, window_size.height); - - sc_screen_update_content_rect(screen); - - if (params->fullscreen) { - sc_screen_switch_fullscreen(screen); - } - #ifdef CONTINUOUS_RESIZING_WORKAROUND SDL_AddEventWatch(event_watcher, screen); #endif @@ -545,7 +533,23 @@ sc_screen_init(struct sc_screen *screen, } static void -sc_screen_show_window(struct sc_screen *screen) { +sc_screen_show_initial_window(struct sc_screen *screen) { + int x = screen->req.x != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.x : (int) SDL_WINDOWPOS_CENTERED; + int y = screen->req.y != SC_WINDOW_POSITION_UNDEFINED + ? screen->req.y : (int) SDL_WINDOWPOS_CENTERED; + + struct sc_size window_size = + get_initial_optimal_size(screen->content_size, screen->req.width, + screen->req.height); + + set_window_size(screen, window_size); + SDL_SetWindowPosition(screen->window, x, y); + + if (screen->req.fullscreen) { + sc_screen_switch_fullscreen(screen); + } + SDL_ShowWindow(screen->window); } @@ -693,6 +697,12 @@ sc_screen_update_frame(struct sc_screen *screen) { } update_texture(screen, frame); + if (!screen->has_frame) { + screen->has_frame = true; + // this is the very first frame, show the window + sc_screen_show_initial_window(screen); + } + sc_screen_render(screen, false); return true; } @@ -763,17 +773,13 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) { bool sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) { switch (event->type) { - case EVENT_NEW_FRAME: - if (!screen->has_frame) { - screen->has_frame = true; - // this is the very first frame, show the window - sc_screen_show_window(screen); - } + case EVENT_NEW_FRAME: { bool ok = sc_screen_update_frame(screen); if (!ok) { LOGW("Frame update failed\n"); } return true; + } case SDL_WINDOWEVENT: if (!screen->has_frame) { // Do nothing diff --git a/app/src/screen.h b/app/src/screen.h index 4810de31e9..3e6c10313e 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -28,6 +28,15 @@ struct sc_screen { struct sc_video_buffer vb; struct fps_counter fps_counter; + // The initial requested window properties + struct { + int16_t x; + int16_t y; + uint16_t width; + uint16_t height; + bool fullscreen; + } req; + SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; @@ -74,10 +83,10 @@ struct sc_screen_params { struct sc_size frame_size; bool always_on_top; - int16_t window_x; - int16_t window_y; - uint16_t window_width; // accepts SC_WINDOW_POSITION_UNDEFINED - uint16_t window_height; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED + int16_t window_y; // accepts SC_WINDOW_POSITION_UNDEFINED + uint16_t window_width; + uint16_t window_height; bool window_borderless; diff --git a/app/src/server.c b/app/src/server.c index ea7e5377e8..b62a74f1ba 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -234,6 +234,10 @@ execute_server(struct sc_server *server, // By default, clipboard_autosync is true ADD_PARAM("clipboard_autosync=false"); } + if (!params->downsize_on_error) { + // By default, downsize_on_error is true + ADD_PARAM("downsize_on_error=false"); + } #undef ADD_PARAM #undef STRBOOL diff --git a/app/src/server.h b/app/src/server.h index 8ea20dc7a0..89cdc2f41a 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -42,6 +42,7 @@ struct sc_server_params { bool force_adb_forward; bool power_off_on_close; bool clipboard_autosync; + bool downsize_on_error; bool tcpip; const char *tcpip_dst; }; diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index ba833a06ce..763a7fadbd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -42,6 +42,11 @@ public interface ClipboardListener { void onClipboardTextChanged(String text); } + private final Size deviceSize; + private final Rect crop; + private int maxSize; + private final int lockVideoOrientation; + private ScreenInfo screenInfo; private RotationListener rotationListener; private ClipboardListener clipboardListener; @@ -69,7 +74,12 @@ public Device(Options options) { int displayInfoFlags = displayInfo.getFlags(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation()); + deviceSize = displayInfo.getSize(); + crop = options.getCrop(); + maxSize = options.getMaxSize(); + lockVideoOrientation = options.getLockVideoOrientation(); + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); layerStack = displayInfo.getLayerStack(); SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { @@ -123,6 +133,11 @@ public void dispatchPrimaryClipChanged() { } } + public synchronized void setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + } + public synchronized ScreenInfo getScreenInfo() { return screenInfo; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 1ac171766d..ef7d85720f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,6 +21,7 @@ public class Options { private String encoderName; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; + private boolean downsizeOnError = true; public Ln.Level getLogLevel() { return logLevel; @@ -149,4 +150,12 @@ public boolean getClipboardAutosync() { public void setClipboardAutosync(boolean clipboardAutosync) { this.clipboardAutosync = clipboardAutosync; } + + public boolean getDownsizeOnError() { + return downsizeOnError; + } + + public void setDownsizeOnError(boolean downsizeOnError) { + this.downsizeOnError = downsizeOnError; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java index ce6b556a7e..06f06a9de8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java @@ -25,6 +25,9 @@ public class ScreenEncoder implements Device.RotationListener { private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"; + // Keep the values in descending order + private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; + private static final int NO_PTS = -1; private final AtomicBoolean rotationChanged = new AtomicBoolean(); @@ -35,14 +38,19 @@ public class ScreenEncoder implements Device.RotationListener { private final int bitRate; private final int maxFps; private final boolean sendFrameMeta; + private final boolean downsizeOnError; private long ptsOrigin; - public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName) { + private boolean firstFrameSent; + + public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List codecOptions, String encoderName, + boolean downsizeOnError) { this.sendFrameMeta = sendFrameMeta; this.bitRate = bitRate; this.maxFps = maxFps; this.codecOptions = codecOptions; this.encoderName = encoderName; + this.downsizeOnError = downsizeOnError; } @Override @@ -91,6 +99,23 @@ private void internalStreamScreen(Device device, FileDescriptor fd) throws IOExc alive = encode(codec, fd); // do not call stop() on exception, it would trigger an IllegalStateException codec.stop(); + } catch (Exception e) { + Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + if (!downsizeOnError || firstFrameSent) { + // Fail immediately + throw e; + } + + int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize()); + if (newMaxSize == 0) { + // Definitively fail + throw e; + } + + // Retry with a smaller device size + Ln.i("Retrying with -m" + newMaxSize + "..."); + device.setMaxSize(newMaxSize); + alive = true; } finally { destroyDisplay(display); codec.release(); @@ -102,6 +127,18 @@ private void internalStreamScreen(Device device, FileDescriptor fd) throws IOExc } } + private static int chooseMaxSizeFallback(Size failedSize) { + int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight()); + for (int value : MAX_SIZE_FALLBACK) { + if (value < currentMaxSize) { + // We found a smaller value to reduce the video size + return value; + } + } + // No fallback, fail definitively + return 0; + } + private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { boolean eof = false; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -122,6 +159,7 @@ private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException { } IO.writeFully(fd, codecBuffer); + firstFrameSent = true; } } finally { if (outputBufferId >= 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java index c27322ef40..8e5b401f1e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenInfo.java @@ -80,15 +80,12 @@ public ScreenInfo withDeviceRotation(int newDeviceRotation) { return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); } - public static ScreenInfo computeScreenInfo(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) { - int rotation = displayInfo.getRotation(); - + public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } - Size deviceSize = displayInfo.getSize(); Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); if (crop != null) { if (rotation % 2 != 0) { // 180s preserve dimensions diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 4f9575ae75..9d7a62e39d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy; import android.graphics.Rect; -import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.os.BatteryManager; import android.os.Build; @@ -70,7 +69,7 @@ private static void scrcpy(Options options) throws IOException { try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward, control)) { ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions, - options.getEncoderName()); + options.getEncoderName(), options.getDownsizeOnError()); Thread controllerThread = null; Thread deviceMessageSenderThread = null; @@ -237,6 +236,10 @@ private static Options createOptions(String... args) { boolean clipboardAutosync = Boolean.parseBoolean(value); options.setClipboardAutosync(clipboardAutosync); break; + case "downsize_on_error": + boolean downsizeOnError = Boolean.parseBoolean(value); + options.setDownsizeOnError(downsizeOnError); + break; default: Ln.w("Unknown server option: " + key); break; @@ -263,16 +266,6 @@ private static Rect parseCrop(String crop) { } private static void suggestFix(Throwable e) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (e instanceof MediaCodec.CodecException) { - MediaCodec.CodecException mce = (MediaCodec.CodecException) e; - if (mce.getErrorCode() == 0xfffffc0e) { - Ln.e("The hardware encoder is not able to encode at the given definition."); - Ln.e("Try with a lower definition:"); - Ln.e(" scrcpy -m 1024"); - } - } - } if (e instanceof InvalidDisplayIdException) { InvalidDisplayIdException idie = (InvalidDisplayIdException) e; int[] displayIds = idie.getAvailableDisplayIds();