Skip to content

Commit bc88915

Browse files
committed
Fix size constraints algorithm
The algorithm used to constrain a size according to the video capabilities, implemented in #6766, was too simplistic: it computed the maximum landscape size by retrieving the maximum width and its associated maximum height (and vice versa for the maximum portrait size). In addition to width and height, encoders are also constrained by a maximum surface area (in blocks). As a result, it is possible for the maximum width to be 4096 with an associated maximum height of 512, while the encoder still supports 1600x1200. In that case, the previous algorithm incorrectly considered 512 to be the absolute maximum height in landscape mode. Instead, determine the optimal supported size, defined as the largest size with the same aspect ratio supported by the encoder. If preserving the aspect ratio is not required (for virtual displays), then the dimensions derived from this optimal size can be extended. Refs #6849 comment <#6849 (comment)> Refs #6766 <#6766> Refs #6848 <#6848> Fixes #6829 <#6829> PR TODO
1 parent 09a5c17 commit bc88915

6 files changed

Lines changed: 154 additions & 164 deletions

File tree

server/src/main/java/com/genymobile/scrcpy/model/Size.java

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package com.genymobile.scrcpy.model;
22

3+
import com.genymobile.scrcpy.util.BinarySearch;
4+
import com.genymobile.scrcpy.util.Ln;
35
import com.genymobile.scrcpy.video.VideoConstraints;
46

57
import android.graphics.Rect;
8+
import android.media.MediaCodecInfo;
9+
import android.util.Range;
610

711
import java.util.Objects;
812

@@ -38,61 +42,84 @@ public Size constrain(VideoConstraints constraints) {
3842
public Size constrain(VideoConstraints constraints, boolean preserveAspectRatio) {
3943
int maxSize = constraints.getMaxSize();
4044
int alignment = constraints.getAlignment();
45+
MediaCodecInfo.VideoCapabilities caps = constraints.getEncoderCapabilities();
4146

4247
assert maxSize >= 0 : "Max size may not be negative";
4348
assert alignment > 0 : "Alignment must be positive";
4449
assert (alignment & (alignment - 1)) == 0 : "Alignment must be a power-of-two";
4550

46-
boolean portrait = width < height;
47-
Size maxCodecSize = portrait ? constraints.getMaxCodecPortraitSize() : constraints.getMaxCodecLandscapeSize();
51+
boolean landscape = width >= height;
52+
int major = landscape ? width : height;
53+
int minor = landscape ? height : width;
4854

49-
int maxWidth = maxCodecSize.width;
50-
int maxHeight = maxCodecSize.height;
51-
if (maxSize > 0) {
52-
if (maxWidth > maxSize) {
53-
maxWidth = maxSize;
54-
}
55-
if (maxHeight > maxSize) {
56-
maxHeight = maxSize;
57-
}
55+
Range<Integer> majorRange = landscape ? caps.getSupportedWidths() : caps.getSupportedHeights();
56+
int minMajor = majorRange.getLower();
57+
int maxMajor = majorRange.getUpper();
58+
if (maxMajor > major) {
59+
// Never increase the size
60+
maxMajor = major;
61+
}
62+
if (maxSize > 0 && maxMajor > maxSize) {
63+
maxMajor = maxSize;
5864
}
59-
maxWidth = align(maxWidth, alignment);
60-
maxHeight = align(maxHeight, alignment);
6165

62-
int w, h;
63-
if (preserveAspectRatio) {
64-
if (width > maxWidth || height > maxHeight) {
65-
if (width * maxHeight > height * maxWidth) {
66-
w = maxWidth;
67-
h = height * maxWidth / width;
68-
} else {
69-
w = width * maxHeight / height;
70-
h = maxHeight;
71-
}
72-
} else {
73-
w = width;
74-
h = height;
75-
}
76-
} else {
77-
w = Math.min(width, maxWidth);
78-
h = Math.min(height, maxHeight);
66+
int minBlock = (minMajor + alignment - 1) / alignment;
67+
int maxBlock = maxMajor / alignment;
68+
69+
int bestBlock = BinarySearch.findHighestTrue(
70+
minBlock, maxBlock, block -> {
71+
int pixels = block * alignment;
72+
int w = align(width * pixels / major, alignment);
73+
int h = align(height * pixels / major, alignment);
74+
return caps.isSizeSupported(w, h);
75+
});
76+
77+
if (bestBlock < minBlock) {
78+
Ln.d("No matching size found, ignore encoder size validation");
79+
bestBlock = maxBlock;
7980
}
8081

81-
w = align(w, alignment);
82-
h = align(h, alignment);
82+
int bestMajor = bestBlock * alignment;
83+
int bestMinor;
84+
85+
if (preserveAspectRatio) {
86+
bestMinor = align(minor * bestMajor / major, alignment);
87+
} else {
88+
// The minor dimension can potentially be extended
89+
int maxMinor = landscape ? caps.getSupportedHeightsFor(bestMajor).getUpper() : caps.getSupportedWidthsFor(bestMajor).getUpper();
90+
if (maxMinor > minor) {
91+
maxMinor = minor;
92+
}
93+
if (maxSize > 0 && maxMinor > maxSize) {
94+
maxMinor = maxSize;
95+
}
96+
bestMinor = align(maxMinor, alignment);
8397

84-
assert w <= maxWidth : "The width cannot exceed maxWidth";
85-
assert h <= maxHeight : "The height cannot exceed maxHeight";
98+
// The major dimension can potentially be extended
99+
maxMajor = landscape ? caps.getSupportedWidthsFor(bestMinor).getUpper() : caps.getSupportedHeightsFor(bestMinor).getUpper();
100+
if (maxMajor > major) {
101+
maxMajor = major;
102+
}
103+
if (maxSize > 0 && maxMajor > maxSize) {
104+
maxMajor = maxSize;
105+
}
106+
bestMajor = align(maxMajor, alignment);
107+
}
86108

87-
// Minimum codec size must be respected (regardless of requested maxSize)
88-
int minCodecSize = alignUp(constraints.getMinCodecSize(), alignment);
89-
if (w < minCodecSize) {
90-
w = minCodecSize;
109+
minMajor = alignUp(minMajor, alignment);
110+
if (bestMajor < minMajor) {
111+
bestMajor = minMajor;
91112
}
92-
if (h < minCodecSize) {
93-
h = minCodecSize;
113+
114+
int minMinor = landscape ? caps.getSupportedHeights().getLower() : caps.getSupportedWidths().getLower();
115+
minMinor = alignUp(minMinor, alignment);
116+
if (bestMinor < minMinor) {
117+
bestMinor = minMinor;
94118
}
95119

120+
int w = landscape ? bestMajor : bestMinor;
121+
int h = landscape ? bestMinor : bestMajor;
122+
96123
assert w % alignment == 0 : "The width must be a multiple of alignment";
97124
assert h % alignment == 0 : "The height must be a multiple of alignment";
98125

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.genymobile.scrcpy.util;
2+
3+
import com.genymobile.scrcpy.AndroidVersions;
4+
5+
import android.annotation.TargetApi;
6+
7+
import java.util.function.Predicate;
8+
9+
public final class BinarySearch {
10+
private BinarySearch() {
11+
// not instantiable
12+
}
13+
14+
/**
15+
* Find the highest value for which the predicate is true.
16+
* <p>
17+
* This assumes that the predicate is monotonic:
18+
* - if it returns {@code false} for a given value, it must return {@code false} for higher values, or equivalently
19+
* - if it returns {@code true} for a given value, it must return {@code true} for lower values.
20+
*
21+
* @param low the lowest value
22+
* @param high the highest value
23+
* @param predicate the predicate
24+
* @return the highest value for which {@code predicate.test(value)} is {@code true}, or {@code low - 1} if not found.
25+
*/
26+
// This function is used for virtual displays, so we don't need to support older versions anyway
27+
@TargetApi(AndroidVersions.API_24_ANDROID_7_0) // for Predicate<T>
28+
public static int findHighestTrue(int low, int high, Predicate<Integer> predicate) {
29+
if (low <= high) {
30+
// Fast path
31+
if (predicate.test(high)) {
32+
return high;
33+
}
34+
--high;
35+
}
36+
37+
int result = low - 1; // low-1 means "not found"
38+
while (low <= high) {
39+
int mid = low + (high - low + 1) / 2;
40+
if (predicate.test(mid)) {
41+
result = mid;
42+
43+
// predicate holds, go right
44+
low = mid + 1;
45+
} else {
46+
// predicate false, go left
47+
high = mid - 1;
48+
}
49+
}
50+
51+
return result;
52+
}
53+
}

server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,7 @@ private static VideoConstraints createVideoConstraints(int maxSize, int minSizeA
7575
Ln.d("Actual video size alignment: " + alignment + "px");
7676
}
7777

78-
int maxLandscapeWidth = caps.getSupportedWidths().getUpper();
79-
int maxLandscapeHeight = caps.getSupportedHeightsFor(maxLandscapeWidth).getUpper();
80-
Size maxLandscapeSize = new Size(maxLandscapeWidth, maxLandscapeHeight);
81-
82-
int maxPortraitHeight = caps.getSupportedHeights().getUpper();
83-
int maxPortraitWidth = caps.getSupportedWidthsFor(maxPortraitHeight).getUpper();
84-
Size maxPortraitSize = new Size(maxPortraitWidth, maxPortraitHeight);
85-
86-
int minWidth = caps.getSupportedWidths().getLower();
87-
int minHeight = caps.getSupportedHeights().getLower();
88-
int minSize = Math.max(minWidth, minHeight);
89-
90-
return new VideoConstraints(maxSize, alignment, maxLandscapeSize, maxPortraitSize, minSize);
78+
return new VideoConstraints(maxSize, alignment, caps);
9179
}
9280

9381
private void streamCapture() throws IOException, ConfigurationException {
Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
11
package com.genymobile.scrcpy.video;
22

3-
import com.genymobile.scrcpy.model.Size;
3+
import android.media.MediaCodecInfo;
44

55
public class VideoConstraints {
66
private final int maxSize;
77
private final int alignment;
8-
private final Size maxCodecLandscapeSize;
9-
private final Size maxCodecPortraitSize;
10-
private final int minCodecSize;
8+
private final MediaCodecInfo.VideoCapabilities caps;
119

12-
public VideoConstraints(int maxSize, int alignment, Size maxCodecLandscapeSize, Size maxCodecPortraitSize, int minCodecSize) {
10+
public VideoConstraints(int maxSize, int alignment, MediaCodecInfo.VideoCapabilities caps) {
1311
assert maxSize >= 0 : "Max size must not be negative";
1412
this.maxSize = maxSize;
1513

1614
assert alignment > 0 : "Alignment must be positive";
1715
assert (alignment & (alignment - 1)) == 0 : "Alignment must be a power-of-two";
1816
this.alignment = alignment;
1917

20-
assert maxCodecLandscapeSize != null;
21-
this.maxCodecLandscapeSize = maxCodecLandscapeSize;
22-
23-
assert maxCodecPortraitSize != null;
24-
this.maxCodecPortraitSize = maxCodecPortraitSize;
25-
26-
assert minCodecSize >= 0;
27-
this.minCodecSize = minCodecSize;
18+
assert caps != null;
19+
this.caps = caps;
2820
}
2921

3022
/**
@@ -48,30 +40,12 @@ public int getAlignment() {
4840
}
4941

5042
/**
51-
* Return the max landscape size supported by the codec.
52-
*
53-
* @return the max landscape size
54-
*/
55-
public Size getMaxCodecLandscapeSize() {
56-
return maxCodecLandscapeSize;
57-
}
58-
59-
/**
60-
* Return the max portrait size supported by the codec.
61-
*
62-
* @return the max portrait size
63-
*/
64-
public Size getMaxCodecPortraitSize() {
65-
return maxCodecPortraitSize;
66-
}
67-
68-
/**
69-
* Return the min size supported by the codec.
43+
* Return the video encoder capabilities.
7044
*
71-
* @return the min size
45+
* @return the video encoder capabilities
7246
*/
73-
public int getMinCodecSize() {
74-
return minCodecSize;
47+
public MediaCodecInfo.VideoCapabilities getEncoderCapabilities() {
48+
return caps;
7549
}
7650

7751
/**
@@ -81,6 +55,6 @@ public int getMinCodecSize() {
8155
* @return the new video constraints
8256
*/
8357
public VideoConstraints withMaxSize(int maxSize) {
84-
return new VideoConstraints(maxSize, alignment, maxCodecLandscapeSize, maxCodecPortraitSize, minCodecSize);
58+
return new VideoConstraints(maxSize, alignment, caps);
8559
}
8660
}

server/src/test/java/com/genymobile/scrcpy/model/SizeTest.java

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.genymobile.scrcpy.util;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
public class BinarySearchTest {
7+
8+
@Test
9+
public void testFindHighestTrue() {
10+
Assert.assertEquals(4, BinarySearch.findHighestTrue(5, 15, i -> false));
11+
Assert.assertEquals(5, BinarySearch.findHighestTrue(5, 15, i -> i < 6));
12+
Assert.assertEquals(6, BinarySearch.findHighestTrue(5, 15, i -> i < 7));
13+
Assert.assertEquals(7, BinarySearch.findHighestTrue(5, 15, i -> i < 8));
14+
Assert.assertEquals(8, BinarySearch.findHighestTrue(5, 15, i -> i < 9));
15+
Assert.assertEquals(9, BinarySearch.findHighestTrue(5, 15, i -> i < 10));
16+
Assert.assertEquals(10, BinarySearch.findHighestTrue(5, 15, i -> i < 11));
17+
Assert.assertEquals(11, BinarySearch.findHighestTrue(5, 15, i -> i < 12));
18+
Assert.assertEquals(12, BinarySearch.findHighestTrue(5, 15, i -> i < 13));
19+
Assert.assertEquals(13, BinarySearch.findHighestTrue(5, 15, i -> i < 14));
20+
Assert.assertEquals(14, BinarySearch.findHighestTrue(5, 15, i -> i < 15));
21+
Assert.assertEquals(15, BinarySearch.findHighestTrue(5, 15, i -> true));
22+
}
23+
}

0 commit comments

Comments
 (0)