Skip to content

Commit 20f3203

Browse files
committed
(Java) Add support for resolution alignment during encoding
1 parent 454c75c commit 20f3203

File tree

5 files changed

+555
-0
lines changed

5 files changed

+555
-0
lines changed

sdk/android/BUILD.gn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@ if (is_android) {
529529
"api/org/webrtc/SoftwareVideoEncoderFactory.java",
530530
"api/org/webrtc/SimulcastVideoEncoder.java",
531531
"api/org/webrtc/SimulcastVideoEncoderFactory.java",
532+
"api/org/webrtc/SimulcastAlignedVideoEncoderFactory.java",
533+
"api/org/webrtc/DefaultAlignedVideoEncoderFactory.java",
534+
"api/org/webrtc/HardwareVideoEncoderWrapper.java",
535+
"api/org/webrtc/ResolutionAdjustment.java",
532536
]
533537

534538
deps = [
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.webrtc;
17+
18+
import java.util.LinkedHashSet;
19+
20+
/**
21+
* The main difference with the standard [DefaultAlignedVideoEncoderFactory] is that this fixes
22+
* issues with resolutions that are not aligned (e.g. VP8 requires 16x16 alignment). You can
23+
* set the alignment by setting [resolutionAdjustment]. Internally the resolution during streaming
24+
* will be cropped to comply with the adjustment. Fallback behaviour is the same as with the
25+
* standard [DefaultVideoEncoderFactory] and it will use the SW encoder if HW fails
26+
* or is not available.
27+
*
28+
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf3042072
29+
* e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/SimulcastVideoEnco
30+
* derFactoryWrapper.kt#L18
31+
*/
32+
public class DefaultAlignedVideoEncoderFactory implements VideoEncoderFactory {
33+
private final VideoEncoderFactory hardwareVideoEncoderFactory;
34+
private final VideoEncoderFactory softwareVideoEncoderFactory;
35+
36+
public DefaultAlignedVideoEncoderFactory(
37+
EglBase.Context eglContext,
38+
boolean enableIntelVp8Encoder,
39+
boolean enableH264HighProfile,
40+
ResolutionAdjustment resolutionAdjustment
41+
) {
42+
VideoEncoderFactory defaultFactory =
43+
new HardwareVideoEncoderFactory(eglContext, enableIntelVp8Encoder, enableH264HighProfile);
44+
hardwareVideoEncoderFactory = (resolutionAdjustment == ResolutionAdjustment.NONE) ?
45+
defaultFactory :
46+
new HardwareVideoEncoderWrapperFactory(defaultFactory, resolutionAdjustment.getValue());
47+
softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
48+
}
49+
50+
@Override
51+
public VideoEncoder createEncoder(VideoCodecInfo info) {
52+
VideoEncoder softwareEncoder = softwareVideoEncoderFactory.createEncoder(info);
53+
VideoEncoder hardwareEncoder = hardwareVideoEncoderFactory.createEncoder(info);
54+
if (hardwareEncoder != null && softwareEncoder != null) {
55+
return new VideoEncoderFallback(softwareEncoder, hardwareEncoder);
56+
}
57+
return hardwareEncoder != null ? hardwareEncoder : softwareEncoder;
58+
}
59+
60+
@Override
61+
public VideoCodecInfo[] getSupportedCodecs() {
62+
LinkedHashSet<VideoCodecInfo> supportedCodecInfos = new LinkedHashSet<>();
63+
supportedCodecInfos.addAll(Arrays.asList(softwareVideoEncoderFactory.getSupportedCodecs()));
64+
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoEncoderFactory.getSupportedCodecs()));
65+
return supportedCodecInfos.toArray(new VideoCodecInfo[0]);
66+
}
67+
}
68+
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.webrtc;
17+
18+
/**
19+
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf304207
20+
* 2e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/HardwareVideoEnco
21+
* derWrapperFactory.kt
22+
*/
23+
class HardwareVideoEncoderWrapper implements VideoEncoder {
24+
25+
private static final String TAG = "HardwareVideoEncoderWrapper";
26+
27+
private final VideoEncoder internalEncoder;
28+
private final int alignment;
29+
30+
public HardwareVideoEncoderWrapper(VideoEncoder internalEncoder, int alignment) {
31+
this.internalEncoder = internalEncoder;
32+
this.alignment = alignment;
33+
}
34+
35+
private static class CropSizeCalculator {
36+
37+
private static final String TAG = "CropSizeCalculator";
38+
39+
private final int alignment;
40+
private final int originalWidth;
41+
private final int originalHeight;
42+
private final int cropX;
43+
private final int cropY;
44+
45+
public CropSizeCalculator(int alignment, int originalWidth, int originalHeight) {
46+
this.alignment = alignment;
47+
this.originalWidth = originalWidth;
48+
this.originalHeight = originalHeight;
49+
this.cropX = originalWidth % alignment;
50+
this.cropY = originalHeight % alignment;
51+
if (originalWidth != 0 && originalHeight != 0) {
52+
Logging.v(TAG + " init(): alignment=" + alignment +
53+
" size=" + originalWidth + "x" + originalHeight + " => " + croppedWidth + "x" + croppedHeight);
54+
}
55+
}
56+
57+
public int getCroppedWidth() {
58+
return originalWidth - cropX;
59+
}
60+
61+
public int getCroppedHeight() {
62+
return originalHeight - cropY;
63+
}
64+
65+
public boolean isCropRequired() {
66+
return cropX != 0 || cropY != 0;
67+
}
68+
69+
public boolean hasFrameSizeChanged(int nextWidth, int nextHeight) {
70+
if (originalWidth == nextWidth && originalHeight == nextHeight) {
71+
return false;
72+
} else {
73+
Logging.v(TAG + " frame size has changed: " +
74+
originalWidth + "x" + originalHeight + " => " + nextWidth + "x" + nextHeight);
75+
return true;
76+
}
77+
return originalWidth != nextWidth || originalHeight != nextHeight;
78+
}
79+
}
80+
81+
private CropSizeCalculator calculator = new CropSizeCalculator(1, 0, 0);
82+
83+
private VideoCodecStatus retryWithoutCropping(int width, int height, Runnable retryFunc) {
84+
Logging.v(TAG, "retrying without resolution adjustment");
85+
calculator = new CropSizeCalculator(1, width, height);
86+
retryFunc.run();
87+
return VideoCodecStatus.OK;
88+
}
89+
90+
@Override
91+
public VideoCodecStatus initEncode(VideoEncoder.Settings originalSettings, VideoEncoder.Callback callback) {
92+
calculator = new CropSizeCalculator(alignment, originalSettings.width, originalSettings.height);
93+
if (!calculator.isCropRequired()) {
94+
return internalEncoder.initEncode(originalSettings, callback);
95+
} else {
96+
VideoEncoder.Settings croppedSettings = new VideoEncoder.Settings(
97+
originalSettings.numberOfCores,
98+
calculator.getCroppedWidth(),
99+
calculator.getCroppedHeight(),
100+
originalSettings.startBitrate,
101+
originalSettings.maxFramerate,
102+
originalSettings.numberOfSimulcastStreams,
103+
originalSettings.automaticResizeOn,
104+
originalSettings.capabilities
105+
);
106+
try {
107+
VideoCodecStatus result = internalEncoder.initEncode(croppedSettings, callback);
108+
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
109+
Logging.e(TAG, "internalEncoder.initEncode() returned FALLBACK_SOFTWARE: " +
110+
"croppedSettings " + croppedSettings);
111+
return retryWithoutCropping(
112+
originalSettings.width,
113+
originalSettings.height,
114+
() -> internalEncoder.initEncode(originalSettings, callback)
115+
);
116+
} else {
117+
return result;
118+
}
119+
} catch (Exception e) {
120+
Logging.e(TAG, "internalEncoder.initEncode() failed", e);
121+
return retryWithoutCropping(
122+
originalSettings.width,
123+
originalSettings.height,
124+
() -> internalEncoder.initEncode(originalSettings, callback)
125+
);
126+
}
127+
}
128+
}
129+
130+
@Override
131+
public VideoCodecStatus release() {
132+
return internalEncoder.release();
133+
}
134+
135+
@Override
136+
public VideoCodecStatus encode(VideoFrame frame, VideoEncoder.EncodeInfo encodeInfo) {
137+
if (calculator.hasFrameSizeChanged(frame.getBuffer().getWidth(), frame.getBuffer().getHeight())) {
138+
calculator = new CropSizeCalculator(alignment, frame.getBuffer().getWidth(), frame.getBuffer().getHeight());
139+
}
140+
if (!calculator.isCropRequired()) {
141+
return internalEncoder.encode(frame, encodeInfo);
142+
} else {
143+
int croppedWidth = calculator.getCroppedWidth();
144+
int croppedHeight = calculator.getCroppedHeight();
145+
VideoFrame.Buffer croppedBuffer = frame.getBuffer().cropAndScale(
146+
calculator.cropX / 2,
147+
calculator.cropY / 2,
148+
croppedWidth,
149+
croppedHeight,
150+
croppedWidth,
151+
croppedHeight
152+
);
153+
VideoFrame croppedFrame = new VideoFrame(croppedBuffer, frame.getRotation(), frame.getTimestampNs());
154+
try {
155+
VideoCodecStatus result = internalEncoder.encode(croppedFrame, encodeInfo);
156+
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
157+
Logging.e(TAG, "internalEncoder.encode() returned FALLBACK_SOFTWARE");
158+
return retryWithoutCropping(
159+
frame.getBuffer().getWidth(),
160+
frame.getBuffer().getHeight(),
161+
() -> internalEncoder.encode(frame, encodeInfo)
162+
);
163+
} else {
164+
return result;
165+
}
166+
} catch (Exception e) {
167+
Logging.e(TAG, "internalEncoder.encode() failed", e);
168+
return retryWithoutCropping(
169+
frame.getBuffer().getWidth(),
170+
frame.getBuffer().getHeight(),
171+
() -> internalEncoder.encode(frame, encodeInfo)
172+
);
173+
} finally {
174+
croppedBuffer.release();
175+
}
176+
}
177+
}
178+
179+
@Override
180+
public VideoCodecStatus setRateAllocation(VideoEncoder.BitrateAllocation allocation, int frameRate) {
181+
return internalEncoder.setRateAllocation(allocation, frameRate);
182+
}
183+
184+
@Override
185+
public VideoEncoder.ScalingSettings getScalingSettings() {
186+
return internalEncoder.getScalingSettings();
187+
}
188+
189+
@Override
190+
public String getImplementationName() {
191+
return internalEncoder.getImplementationName();
192+
}
193+
194+
@Override
195+
public long createNativeVideoEncoder() {
196+
return internalEncoder.createNativeVideoEncoder();
197+
}
198+
199+
@Override
200+
public boolean isHardwareEncoder() {
201+
return internalEncoder.isHardwareEncoder();
202+
}
203+
204+
@Override
205+
public VideoCodecStatus setRates(VideoEncoder.RateControlParameters rcParameters) {
206+
return internalEncoder.setRates(rcParameters);
207+
}
208+
209+
@Override
210+
public VideoEncoder.ResolutionBitrateLimits[] getResolutionBitrateLimits() {
211+
return internalEncoder.getResolutionBitrateLimits();
212+
}
213+
214+
@Override
215+
public VideoEncoder.EncoderInfo getEncoderInfo() {
216+
return internalEncoder.getEncoderInfo();
217+
}
218+
}
219+
220+
class HardwareVideoEncoderWrapperFactory implements VideoEncoderFactory {
221+
222+
private static final String TAG = "HardwareVideoEncoderWrapperFactory";
223+
224+
private final HardwareVideoEncoderFactory factory;
225+
private final int resolutionPixelAlignment;
226+
227+
public HardwareVideoEncoderWrapperFactory(HardwareVideoEncoderFactory factory, int resolutionPixelAlignment) {
228+
this.factory = factory;
229+
this.resolutionPixelAlignment = resolutionPixelAlignment;
230+
if (resolutionPixelAlignment == 0) {
231+
throw new IllegalArgumentException("resolutionPixelAlignment should not be 0");
232+
}
233+
}
234+
235+
@Override
236+
public VideoEncoder createEncoder(VideoCodecInfo videoCodecInfo) {
237+
try {
238+
VideoEncoder encoder = factory.createEncoder(videoCodecInfo);
239+
if (encoder == null) {
240+
return null;
241+
}
242+
return new HardwareVideoEncoderWrapper(encoder, resolutionPixelAlignment);
243+
} catch (Exception e) {
244+
Logging.e(TAG, "createEncoder failed", e);
245+
return null;
246+
}
247+
}
248+
249+
@Override
250+
public VideoCodecInfo[] getSupportedCodecs() {
251+
return factory.getSupportedCodecs();
252+
}
253+
}
254+
255+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.webrtc;
17+
18+
/**
19+
* Resolution alignment values. Generally the MULTIPLE_OF_16 is recommended
20+
* for both VP8 and H264
21+
*/
22+
public enum ResolutionAdjustment {
23+
NONE(1),
24+
MULTIPLE_OF_2(2),
25+
MULTIPLE_OF_4(4),
26+
MULTIPLE_OF_8(8),
27+
MULTIPLE_OF_16(16);
28+
29+
private final int value;
30+
31+
private ResolutionAdjustment(int value) {
32+
this.value = value;
33+
}
34+
35+
public int getValue() {
36+
return value;
37+
}
38+
}
39+

0 commit comments

Comments
 (0)