Support custom virtual display refresh rates

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
This commit is contained in:
Kaiming Hu 2024-11-20 15:33:54 +08:00 committed by Romain Vimont
parent 1b5d88368a
commit 7a86156503
8 changed files with 225 additions and 29 deletions

View File

@ -323,7 +323,8 @@ Create a new display with the specified resolution and density. If not provided,
Examples: Examples:
\-\-new\-display=1920x1080 \-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420 \-\-new\-display=1920x1080/420 # force 420 dpi
\-\-new\-display=1920x1080@24 # 24 fps (Android >= 14)
\-\-new\-display # main display size and density \-\-new\-display # main display size and density
\-\-new\-display=/240 # main display size and 240 dpi \-\-new\-display=/240 # main display size and 240 dpi

View File

@ -586,14 +586,17 @@ static const struct sc_option options[] = {
{ {
.longopt_id = OPT_NEW_DISPLAY, .longopt_id = OPT_NEW_DISPLAY,
.longopt = "new-display", .longopt = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]", .argdesc = "[<width>x<height>][/<dpi>][@<fps>]",
.optional_arg = true, .optional_arg = true,
.text = "Create a new display with the specified resolution and " .text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display " "density. If not provided, they default to the main display "
"dimensions and DPI.\n" "dimensions and DPI.\n"
"From Android 14, it is also possible to request a frame rate. "
"If not provided, it defaults to 60 fps.\n"
"Examples:\n" "Examples:\n"
" --new-display=1920x1080\n" " --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n" " --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display=1920x1080@24 # 24 fps (Android >= 14)\n"
" --new-display # main display size and density\n" " --new-display # main display size and density\n"
" --new-display=/240 # main display size and 240 dpi", " --new-display=/240 # main display size and 240 dpi",
}, },

View File

@ -7,6 +7,7 @@ To mirror a new virtual display instead of the device screen:
```bash ```bash
scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080
scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display=1920x1080/420 # force 420 dpi
scrcpy --new-display=1920x1080@24 # 24 fps (Android >= 14)
scrcpy --new-display # use the main display size and density scrcpy --new-display # use the main display size and density
scrcpy --new-display=/240 # use the main display size and 240 dpi scrcpy --new-display=/240 # use the main display size and 240 dpi
``` ```

View File

@ -566,36 +566,68 @@ public class Options {
} }
} }
private static NewDisplay parseNewDisplay(String newDisplay) { static NewDisplay parseNewDisplay(String newDisplay) {
// Possible inputs: // Input in the form "[<width>x<height>][/<dpi>][@<fps>]" (each [] block is optional)
// - "" (empty string) // For convenience, the order of dpi and fps does not matter.
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
if (newDisplay.isEmpty()) { if (newDisplay.isEmpty()) {
return new NewDisplay(); return new NewDisplay();
} }
String[] tokens = newDisplay.split("/"); String sizeString = null;
String dpiString = null;
String fpsString = null;
Size size; String s = newDisplay;
if (!tokens[0].isEmpty()) { while (true) {
size = parseSize(tokens[0]); int slashIndex = s.indexOf('/');
} else { int atIndex = s.indexOf('@');
size = null; int lastSepIndex = Math.max(slashIndex, atIndex);
} if (lastSepIndex == -1) {
if (!s.isEmpty()) {
int dpi; sizeString = s;
if (tokens.length >= 2) { }
dpi = Integer.parseInt(tokens[1]); break;
if (dpi <= 0) { } else {
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]); char lastSep = newDisplay.charAt(lastSepIndex);
if (lastSep == '@') {
if (fpsString != null) {
throw new IllegalArgumentException("Invalid new display format: '@' may not appear twice");
}
fpsString = s.substring(lastSepIndex + 1);
} else {
assert lastSep == '/';
if (dpiString != null) {
throw new IllegalArgumentException("Invalid new display format: '/' may not appear twice");
}
dpiString = s.substring(lastSepIndex + 1);
}
s = s.substring(0, lastSepIndex);
} }
} else {
dpi = 0;
} }
return new NewDisplay(size, dpi); Size size = null;
int dpi = 0;
float fps = 0;
if (sizeString != null) {
size = parseSize(sizeString);
}
if (dpiString != null) {
dpi = Integer.parseInt(dpiString);
if (dpi <= 0) {
throw new IllegalArgumentException("Invalid non-positive dpi: " + dpiString);
}
}
if (fpsString != null) {
fps = Float.parseFloat(fpsString);
if (fps < 0) {
throw new IllegalArgumentException("Invalid negative fps: " + fpsString);
}
}
return new NewDisplay(size, dpi, fps);
} }
private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) { private static Pair<Orientation.Lock, Orientation> parseCaptureOrientation(String value) {

View File

@ -3,14 +3,16 @@ package com.genymobile.scrcpy.device;
public final class NewDisplay { public final class NewDisplay {
private Size size; private Size size;
private int dpi; private int dpi;
private float fps;
public NewDisplay() { public NewDisplay() {
// Auto size and dpi // Auto size, dpi and fps
} }
public NewDisplay(Size size, int dpi) { public NewDisplay(Size size, int dpi, float fps) {
this.size = size; this.size = size;
this.dpi = dpi; this.dpi = dpi;
this.fps = fps;
} }
public Size getSize() { public Size getSize() {
@ -21,6 +23,10 @@ public final class NewDisplay {
return dpi; return dpi;
} }
public float getFps() {
return fps;
}
public boolean hasExplicitSize() { public boolean hasExplicitSize() {
return size != null; return size != null;
} }
@ -28,4 +34,8 @@ public final class NewDisplay {
public boolean hasExplicitDpi() { public boolean hasExplicitDpi() {
return dpi != 0; return dpi != 0;
} }
public boolean hasExplicitFps() {
return fps != 0;
}
} }

View File

@ -14,8 +14,10 @@ import com.genymobile.scrcpy.util.AffineMatrix;
import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.graphics.Rect; import android.graphics.Rect;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Build; import android.os.Build;
import android.view.Surface; import android.view.Surface;
@ -161,6 +163,7 @@ public class NewDisplayCapture extends SurfaceCapture {
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
} }
@SuppressLint("WrongConstant")
public void startNew(Surface surface) { public void startNew(Surface surface) {
int virtualDisplayId; int virtualDisplayId;
try { try {
@ -182,10 +185,30 @@ public class NewDisplayCapture extends SurfaceCapture {
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
} }
} }
virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); // Since Android 14, it is possible to request a display frame rate:
// <https://android.googlesource.com/platform/frameworks/base/+/6c57176e9a2882eff03c5b3f3cccfd988d38488d>
// It defaults to 60 fps:
// <https://android.googlesource.com/platform/frameworks/base/+/6c57176e9a2882eff03c5b3f3cccfd988d38488d/services/core/java/com/android/server/display/VirtualDisplayAdapter.java#562>
float fps = newDisplay.getFps();
if (fps > 0) {
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder(
"scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi);
builder.setFlags(flags);
builder.setSurface(surface);
builder.setRequestedRefreshRate(fps);
virtualDisplay = ServiceManager.getDisplayManager().createNewVirtualDisplay(builder.build());
} else {
throw new UnsupportedOperationException("Setting the virtual display frame rate (@" + fps + ") requires Android >= 14");
}
} else {
virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags);
}
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); String fpsString = fps > 0 ? "@" + fps : "";
Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + fpsString + " (id=" + virtualDisplayId + ")");
displaySizeMonitor.start(virtualDisplayId, this::invalidate); displaySizeMonitor.start(virtualDisplayId, this::invalidate);
} catch (Exception e) { } catch (Exception e) {

View File

@ -11,6 +11,7 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Handler; import android.os.Handler;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
@ -174,6 +175,11 @@ public final class DisplayManager {
return getAndroidDisplayManager().createVirtualDisplay(name, width, height, dpi, surface, flags); return getAndroidDisplayManager().createVirtualDisplay(name, width, height, dpi, surface, flags);
} }
@TargetApi(AndroidVersions.API_34_ANDROID_14)
public VirtualDisplay createNewVirtualDisplay(VirtualDisplayConfig config) throws ReflectiveOperationException {
return getAndroidDisplayManager().createVirtualDisplay(config);
}
private Method getRequestDisplayPowerMethod() throws NoSuchMethodException { private Method getRequestDisplayPowerMethod() throws NoSuchMethodException {
if (requestDisplayPowerMethod == null) { if (requestDisplayPowerMethod == null) {
requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class); requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class);

View File

@ -0,0 +1,120 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.device.NewDisplay;
import com.genymobile.scrcpy.device.Size;
import org.junit.Assert;
import org.junit.Test;
public class OptionsTest {
@Test
public void testParseNewDisplayEmpty() {
NewDisplay newDisplay = Options.parseNewDisplay("");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayDpiOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("/240");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayFpsOnly() {
NewDisplay newDisplay = Options.parseNewDisplay("@30");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080/240");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertFalse(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(0, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080@30");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertFalse(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(0, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndDpiAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080/240@30");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplaySizeAndFpsAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("1920x1080@30/240");
Assert.assertTrue(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertEquals(new Size(1920, 1080), newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayDpiAndFps() {
NewDisplay newDisplay = Options.parseNewDisplay("/240@30");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
@Test
public void testParseNewDisplayFpsAndDpi() {
NewDisplay newDisplay = Options.parseNewDisplay("@30/240");
Assert.assertFalse(newDisplay.hasExplicitSize());
Assert.assertTrue(newDisplay.hasExplicitDpi());
Assert.assertTrue(newDisplay.hasExplicitFps());
Assert.assertNull(newDisplay.getSize());
Assert.assertEquals(240, newDisplay.getDpi());
Assert.assertEquals(30, newDisplay.getFps(), 0);
}
}