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

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