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:
\-\-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=/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 = "new-display",
.argdesc = "[<width>x<height>][/<dpi>]",
.argdesc = "[<width>x<height>][/<dpi>][@<fps>]",
.optional_arg = true,
.text = "Create a new display with the specified resolution and "
"density. If not provided, they default to the main display "
"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"
" --new-display=1920x1080\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=/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
scrcpy --new-display=1920x1080
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=/240 # use the main display size and 240 dpi
```

View File

@ -566,36 +566,68 @@ public class Options {
}
}
private static NewDisplay parseNewDisplay(String newDisplay) {
// Possible inputs:
// - "" (empty string)
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
static NewDisplay parseNewDisplay(String newDisplay) {
// Input in the form "[<width>x<height>][/<dpi>][@<fps>]" (each [] block is optional)
// For convenience, the order of dpi and fps does not matter.
if (newDisplay.isEmpty()) {
return new NewDisplay();
}
String[] tokens = newDisplay.split("/");
String sizeString = null;
String dpiString = null;
String fpsString = null;
Size size;
if (!tokens[0].isEmpty()) {
size = parseSize(tokens[0]);
} else {
size = null;
}
int dpi;
if (tokens.length >= 2) {
dpi = Integer.parseInt(tokens[1]);
if (dpi <= 0) {
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]);
String s = newDisplay;
while (true) {
int slashIndex = s.indexOf('/');
int atIndex = s.indexOf('@');
int lastSepIndex = Math.max(slashIndex, atIndex);
if (lastSepIndex == -1) {
if (!s.isEmpty()) {
sizeString = s;
}
break;
} else {
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) {

View File

@ -3,14 +3,16 @@ package com.genymobile.scrcpy.device;
public final class NewDisplay {
private Size size;
private int dpi;
private float fps;
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.dpi = dpi;
this.fps = fps;
}
public Size getSize() {
@ -21,6 +23,10 @@ public final class NewDisplay {
return dpi;
}
public float getFps() {
return fps;
}
public boolean hasExplicitSize() {
return size != null;
}
@ -28,4 +34,8 @@ public final class NewDisplay {
public boolean hasExplicitDpi() {
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.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Build;
import android.view.Surface;
@ -161,6 +163,7 @@ public class NewDisplayCapture extends SurfaceCapture {
displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform);
}
@SuppressLint("WrongConstant")
public void startNew(Surface surface) {
int virtualDisplayId;
try {
@ -182,10 +185,30 @@ public class NewDisplayCapture extends SurfaceCapture {
| 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();
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);
} catch (Exception e) {

View File

@ -11,6 +11,7 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.hardware.display.VirtualDisplay;
import android.hardware.display.VirtualDisplayConfig;
import android.os.Handler;
import android.view.Display;
import android.view.Surface;
@ -174,6 +175,11 @@ public final class DisplayManager {
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 {
if (requestDisplayPowerMethod == null) {
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);
}
}