From 4f97e2e30b9b02ff43d64e940165acafd7512a23 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 8 Apr 2026 20:11:15 +0200 Subject: [PATCH] Disable retry capture at lower resolution A mechanism was introduced to retry capture at a lower resolution to support devices unable to encode at the device screen resolution. While useful, this approach is inherently limited and will not be able to handle the dynamic resizing required for resizable virtual displays. Disable this mechanism entirely. Further commits will add support for adjusting the size in advance according to video encoder capabilities. Refs #2947 PR #6766 --- app/data/bash-completion/scrcpy | 1 - app/data/zsh-completion/_scrcpy | 1 - app/scrcpy.1 | 6 -- app/src/cli.c | 24 +----- app/src/options.c | 1 - app/src/options.h | 1 - app/src/scrcpy.c | 1 - app/src/server.c | 4 - app/src/server.h | 1 - doc/video.md | 3 - .../java/com/genymobile/scrcpy/Options.java | 8 -- .../scrcpy/video/CameraCapture.java | 12 +-- .../scrcpy/video/NewDisplayCapture.java | 8 +- .../scrcpy/video/ScreenCapture.java | 8 +- .../scrcpy/video/SurfaceCapture.java | 7 -- .../scrcpy/video/SurfaceEncoder.java | 76 ------------------- 16 files changed, 6 insertions(+), 156 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index eedbd504..3f4c649d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -56,7 +56,6 @@ _scrcpy() { --no-audio-playback --no-cleanup --no-clipboard-autosync - --no-downsize-on-error --no-key-repeat --no-mipmaps --no-mouse-hover diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4bf3f9dc..e106f8a3 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -62,7 +62,6 @@ arguments=( '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]' - '--no-downsize-on-error[Disable lowering definition on MediaCodec error]' '--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-mipmaps[Disable the generation of mipmaps]' '--no-mouse-hover[Do not forward mouse hover events]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e2a65bae..b6423c28 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -384,12 +384,6 @@ 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 \-\-no\-key\-repeat Do not forward repeated key events when a key is held down. diff --git a/app/src/cli.c b/app/src/cli.c index 8c5e09c9..e4076529 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -51,7 +51,6 @@ enum { OPT_NO_CLIPBOARD_AUTOSYNC, OPT_TCPIP, OPT_RAW_KEY_EVENTS, - OPT_NO_DOWNSIZE_ON_ERROR, OPT_OTG, OPT_NO_CLEANUP, OPT_PRINT_FPS, @@ -611,13 +610,6 @@ static const struct sc_option options[] = { "it changes.\n" "This option disables this automatic synchronization." }, - { - .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_KEY_REPEAT, .longopt = "no-key-repeat", @@ -2581,9 +2573,6 @@ 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_NO_VIDEO: opts->video = false; break; @@ -2856,16 +2845,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } #ifdef HAVE_V4L2 - if (v4l2) { - if (!opts->video) { - LOGE("V4L2 sink requires video capture, but --no-video was set."); - return false; - } - - // 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 (v4l2 && !opts->video) { + LOGE("V4L2 sink requires video capture, but --no-video was set."); + return false; } if (opts->v4l2_buffer && !opts->v4l2_device) { diff --git a/app/src/options.c b/app/src/options.c index 9ef1bce7..10ad471f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -94,7 +94,6 @@ 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, .select_tcpip = false, diff --git a/app/src/options.h b/app/src/options.h index abd09d61..752d5870 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -304,7 +304,6 @@ struct scrcpy_options { bool legacy_paste; bool power_off_on_close; bool clipboard_autosync; - bool downsize_on_error; bool tcpip; const char *tcpip_dst; bool select_usb; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8bb428c9..d067b372 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -455,7 +455,6 @@ 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, .cleanup = options->cleanup, diff --git a/app/src/server.c b/app/src/server.c index df6e2b1f..7a33057a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -401,10 +401,6 @@ 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"); - } if (!params->cleanup) { // By default, cleanup is true ADD_PARAM("cleanup=false"); diff --git a/app/src/server.h b/app/src/server.h index 03517233..ebb1f137 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -61,7 +61,6 @@ 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; bool select_usb; diff --git a/doc/video.md b/doc/video.md index 8cea0373..7b1e4c03 100644 --- a/doc/video.md +++ b/doc/video.md @@ -24,9 +24,6 @@ scrcpy -m 1024 # short version The other dimension is computed so that the Android device aspect ratio is preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. -If encoding fails, scrcpy automatically tries again with a lower definition -(unless `--no-downsize-on-error` is enabled). - For camera mirroring, the `--max-size` value is used to select the camera source size instead (among the available resolutions). diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index d2c324f6..aba8251e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -60,7 +60,6 @@ public class Options { private String audioEncoder; private boolean powerOffScreenOnClose; private boolean clipboardAutosync = true; - private boolean downsizeOnError = true; private boolean cleanup = true; private boolean powerOn = true; @@ -231,10 +230,6 @@ public class Options { return clipboardAutosync; } - public boolean getDownsizeOnError() { - return downsizeOnError; - } - public boolean getCleanup() { return cleanup; } @@ -443,9 +438,6 @@ public class Options { case "clipboard_autosync": options.clipboardAutosync = Boolean.parseBoolean(value); break; - case "downsize_on_error": - options.downsizeOnError = Boolean.parseBoolean(value); - break; case "cleanup": options.cleanup = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 4c85b185..b9fbd9b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -59,7 +59,7 @@ public class CameraCapture extends SurfaceCapture { private final String explicitCameraId; private final CameraFacing cameraFacing; private final Size explicitSize; - private int maxSize; + private final int maxSize; private final CameraAspectRatio aspectRatio; private final int fps; private final boolean highSpeed; @@ -374,16 +374,6 @@ public class CameraCapture extends SurfaceCapture { return videoSize; } - @Override - public boolean setMaxSize(int maxSize) { - if (explicitSize != null) { - return false; - } - - this.maxSize = maxSize; - return true; - } - @SuppressLint("MissingPermission") @TargetApi(AndroidVersions.API_31_ANDROID_12) private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { 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 5dab6030..cc9e8c6e 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 final int maxSize; private final int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; @@ -251,12 +251,6 @@ public class NewDisplayCapture extends SurfaceCapture { return videoSize; } - @Override - public synchronized boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - return true; - } - private static int scaleDpi(Size initialSize, int initialDpi, Size size) { int den = initialSize.getMax(); int num = size.getMax(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 4586f474..81db7159 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -29,7 +29,7 @@ public class ScreenCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final int displayId; - private int maxSize; + private final int maxSize; private final Rect crop; private Orientation.Lock captureOrientationLock; private Orientation captureOrientation; @@ -187,12 +187,6 @@ public class ScreenCapture extends SurfaceCapture { return videoSize; } - @Override - public boolean setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - return true; - } - private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 81da337b..2b120884 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -73,13 +73,6 @@ public abstract class SurfaceCapture { */ public abstract Size getSize(); - /** - * Set the maximum capture size (set by the encoder if it does not support the current size). - * - * @param maxSize Maximum size - */ - public abstract boolean setMaxSize(int maxSize); - /** * Indicate if the capture has been closed internally. * 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 e61d4d70..b1da801d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -18,7 +18,6 @@ import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Build; import android.os.Looper; -import android.os.SystemClock; import android.view.Surface; import java.io.IOException; @@ -32,22 +31,14 @@ public class SurfaceEncoder implements AsyncProcessor { 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 MAX_CONSECUTIVE_ERRORS = 3; - private final SurfaceCapture capture; private final Streamer streamer; private final String encoderName; private final List codecOptions; private final int videoBitRate; private final float maxFps; - private final boolean downsizeOnError; private final int minSizeAlignment; - private boolean firstFrameSent; - private int consecutiveErrors; - private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); @@ -60,7 +51,6 @@ public class SurfaceEncoder implements AsyncProcessor { this.maxFps = options.getMaxFps(); this.codecOptions = options.getVideoCodecOptions(); this.encoderName = options.getVideoEncoder(); - this.downsizeOnError = options.getDownsizeOnError(); this.minSizeAlignment = options.getMinSizeAlignment(); } @@ -121,16 +111,6 @@ public class SurfaceEncoder implements AsyncProcessor { // The capture might have been closed internally (for example if the camera is disconnected) alive = !stopped.get() && !capture.isClosed(); } - } catch (IllegalStateException | IllegalArgumentException | IOException e) { - if (IO.isBrokenPipe(e)) { - // Do not retry on broken pipe, which is expected on close because the socket is closed by the client - throw e; - } - Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); - if (!prepareRetry(size)) { - throw e; - } - alive = true; } finally { reset.setRunningMediaCodec(null); if (captureStarted) { @@ -155,54 +135,6 @@ public class SurfaceEncoder implements AsyncProcessor { } } - private boolean prepareRetry(Size currentSize) { - if (firstFrameSent) { - ++consecutiveErrors; - if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { - // Definitively fail - return false; - } - - // Wait a bit to increase the probability that retrying will fix the problem - SystemClock.sleep(50); - return true; - } - - if (!downsizeOnError) { - // Must fail immediately - return false; - } - - // Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) - - int newMaxSize = chooseMaxSizeFallback(currentSize); - if (newMaxSize == 0) { - // Must definitively fail - return false; - } - - boolean accepted = capture.setMaxSize(newMaxSize); - if (!accepted) { - return false; - } - - // Retry with a smaller size - Ln.i("Retrying with -m" + newMaxSize + "..."); - return true; - } - - 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 void encode(MediaCodec codec, Streamer streamer) throws IOException { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -214,14 +146,6 @@ public class SurfaceEncoder implements AsyncProcessor { // On EOS, there might be data or not, depending on bufferInfo.size if (outputBufferId >= 0 && bufferInfo.size > 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); - - boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; - if (!isConfig) { - // If this is not a config packet, then it contains a frame - firstFrameSent = true; - consecutiveErrors = 0; - } - streamer.writePacket(codecBuffer, bufferInfo); } } finally {