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 <https://github.com/Genymobile/scrcpy/pull/2947>
PR #6766 <https://github.com/Genymobile/scrcpy/pull/6766>
This commit is contained in:
Romain Vimont 2026-04-08 20:11:15 +02:00
parent 3b00032a01
commit 4f97e2e30b
16 changed files with 6 additions and 156 deletions

View file

@ -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

View file

@ -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]'

View file

@ -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.

View file

@ -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) {

View file

@ -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,

View file

@ -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;

View file

@ -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,

View file

@ -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");

View file

@ -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;

View file

@ -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).

View file

@ -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;

View file

@ -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 {

View file

@ -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();

View file

@ -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".

View file

@ -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.
*

View file

@ -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<CodecOption> 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 {