mirror of https://github.com/Genymobile/scrcpy
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:
parent
1b5d88368a
commit
7a86156503
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue