Skip to content

Commit c3b9942

Browse files
authored
(Java) Add support for resolution alignment during encoding (#25)
* (Java) Add support for resolution alignment during encoding * rename native to delegate * extract HardwareVideoEncoderWrapperFactory * change class order * add missing import * compile fixes * fix logging * fix unreachable return statement * fix dependencies * move ResolutionAdjustment to video_java
1 parent 454c75c commit c3b9942

6 files changed

+577
-0
lines changed

sdk/android/BUILD.gn

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ if (is_android) {
232232
"api/org/webrtc/WrappedNativeVideoEncoder.java",
233233
"api/org/webrtc/YuvConverter.java",
234234
"api/org/webrtc/YuvHelper.java",
235+
"api/org/webrtc/ResolutionAdjustment.java",
235236
"src/java/org/webrtc/EglBase10Impl.java",
236237
"src/java/org/webrtc/EglBase14Impl.java",
237238
"src/java/org/webrtc/GlGenericDrawer.java",
@@ -363,6 +364,8 @@ if (is_android) {
363364
"api/org/webrtc/DefaultVideoDecoderFactory.java",
364365
"api/org/webrtc/DefaultVideoEncoderFactory.java",
365366
"api/org/webrtc/WrappedVideoDecoderFactory.java",
367+
"api/org/webrtc/DefaultAlignedVideoEncoderFactory.java",
368+
"api/org/webrtc/SimulcastAlignedVideoEncoderFactory.java",
366369
]
367370

368371
deps = [
@@ -394,6 +397,8 @@ if (is_android) {
394397
sources = [
395398
"api/org/webrtc/HardwareVideoDecoderFactory.java",
396399
"api/org/webrtc/HardwareVideoEncoderFactory.java",
400+
"api/org/webrtc/HardwareVideoEncoderWrapper.java",
401+
"api/org/webrtc/HardwareVideoEncoderWrapperFactory.java",
397402
"api/org/webrtc/PlatformSoftwareVideoDecoderFactory.java",
398403
"src/java/org/webrtc/AndroidVideoDecoder.java",
399404
"src/java/org/webrtc/BaseBitrateAdjuster.java",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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.Arrays;
19+
import java.util.LinkedHashSet;
20+
21+
/**
22+
* The main difference with the standard [DefaultAlignedVideoEncoderFactory] is that this fixes
23+
* issues with resolutions that are not aligned (e.g. VP8 requires 16x16 alignment). You can
24+
* set the alignment by setting [resolutionAdjustment]. Internally the resolution during streaming
25+
* will be cropped to comply with the adjustment. Fallback behaviour is the same as with the
26+
* standard [DefaultVideoEncoderFactory] and it will use the SW encoder if HW fails
27+
* or is not available.
28+
*
29+
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf3042072
30+
* e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/SimulcastVideoEnco
31+
* derFactoryWrapper.kt#L18
32+
*/
33+
public class DefaultAlignedVideoEncoderFactory implements VideoEncoderFactory {
34+
private final VideoEncoderFactory hardwareVideoEncoderFactory;
35+
private final VideoEncoderFactory softwareVideoEncoderFactory;
36+
37+
public DefaultAlignedVideoEncoderFactory(
38+
EglBase.Context eglContext,
39+
boolean enableIntelVp8Encoder,
40+
boolean enableH264HighProfile,
41+
ResolutionAdjustment resolutionAdjustment
42+
) {
43+
HardwareVideoEncoderFactory defaultFactory =
44+
new HardwareVideoEncoderFactory(eglContext, enableIntelVp8Encoder, enableH264HighProfile);
45+
hardwareVideoEncoderFactory = (resolutionAdjustment == ResolutionAdjustment.NONE) ?
46+
defaultFactory :
47+
new HardwareVideoEncoderWrapperFactory(defaultFactory, resolutionAdjustment.getValue());
48+
softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
49+
}
50+
51+
@Override
52+
public VideoEncoder createEncoder(VideoCodecInfo info) {
53+
VideoEncoder softwareEncoder = softwareVideoEncoderFactory.createEncoder(info);
54+
VideoEncoder hardwareEncoder = hardwareVideoEncoderFactory.createEncoder(info);
55+
if (hardwareEncoder != null && softwareEncoder != null) {
56+
return new VideoEncoderFallback(softwareEncoder, hardwareEncoder);
57+
}
58+
return hardwareEncoder != null ? hardwareEncoder : softwareEncoder;
59+
}
60+
61+
@Override
62+
public VideoCodecInfo[] getSupportedCodecs() {
63+
LinkedHashSet<VideoCodecInfo> supportedCodecInfos = new LinkedHashSet<>();
64+
supportedCodecInfos.addAll(Arrays.asList(softwareVideoEncoderFactory.getSupportedCodecs()));
65+
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoEncoderFactory.getSupportedCodecs()));
66+
return supportedCodecInfos.toArray(new VideoCodecInfo[0]);
67+
}
68+
}
69+
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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 + " => " + getCroppedWidth() + "x" + getCroppedHeight());
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+
}
78+
}
79+
80+
private CropSizeCalculator calculator = new CropSizeCalculator(1, 0, 0);
81+
82+
private VideoCodecStatus retryWithoutCropping(int width, int height, Runnable retryFunc) {
83+
Logging.v(TAG, "retrying without resolution adjustment");
84+
calculator = new CropSizeCalculator(1, width, height);
85+
retryFunc.run();
86+
return VideoCodecStatus.OK;
87+
}
88+
89+
@Override
90+
public VideoCodecStatus initEncode(VideoEncoder.Settings originalSettings, VideoEncoder.Callback callback) {
91+
calculator = new CropSizeCalculator(alignment, originalSettings.width, originalSettings.height);
92+
if (!calculator.isCropRequired()) {
93+
return internalEncoder.initEncode(originalSettings, callback);
94+
} else {
95+
VideoEncoder.Settings croppedSettings = new VideoEncoder.Settings(
96+
originalSettings.numberOfCores,
97+
calculator.getCroppedWidth(),
98+
calculator.getCroppedHeight(),
99+
originalSettings.startBitrate,
100+
originalSettings.maxFramerate,
101+
originalSettings.numberOfSimulcastStreams,
102+
originalSettings.automaticResizeOn,
103+
originalSettings.capabilities
104+
);
105+
try {
106+
VideoCodecStatus result = internalEncoder.initEncode(croppedSettings, callback);
107+
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
108+
Logging.e(TAG, "internalEncoder.initEncode() returned FALLBACK_SOFTWARE: " +
109+
"croppedSettings " + croppedSettings);
110+
return retryWithoutCropping(
111+
originalSettings.width,
112+
originalSettings.height,
113+
() -> internalEncoder.initEncode(originalSettings, callback)
114+
);
115+
} else {
116+
return result;
117+
}
118+
} catch (Exception e) {
119+
Logging.e(TAG, "internalEncoder.initEncode() failed", e);
120+
return retryWithoutCropping(
121+
originalSettings.width,
122+
originalSettings.height,
123+
() -> internalEncoder.initEncode(originalSettings, callback)
124+
);
125+
}
126+
}
127+
}
128+
129+
@Override
130+
public VideoCodecStatus release() {
131+
return internalEncoder.release();
132+
}
133+
134+
@Override
135+
public VideoCodecStatus encode(VideoFrame frame, VideoEncoder.EncodeInfo encodeInfo) {
136+
if (calculator.hasFrameSizeChanged(frame.getBuffer().getWidth(), frame.getBuffer().getHeight())) {
137+
calculator = new CropSizeCalculator(alignment, frame.getBuffer().getWidth(), frame.getBuffer().getHeight());
138+
}
139+
if (!calculator.isCropRequired()) {
140+
return internalEncoder.encode(frame, encodeInfo);
141+
} else {
142+
int croppedWidth = calculator.getCroppedWidth();
143+
int croppedHeight = calculator.getCroppedHeight();
144+
VideoFrame.Buffer croppedBuffer = frame.getBuffer().cropAndScale(
145+
calculator.cropX / 2,
146+
calculator.cropY / 2,
147+
croppedWidth,
148+
croppedHeight,
149+
croppedWidth,
150+
croppedHeight
151+
);
152+
VideoFrame croppedFrame = new VideoFrame(croppedBuffer, frame.getRotation(), frame.getTimestampNs());
153+
try {
154+
VideoCodecStatus result = internalEncoder.encode(croppedFrame, encodeInfo);
155+
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
156+
Logging.e(TAG, "internalEncoder.encode() returned FALLBACK_SOFTWARE");
157+
return retryWithoutCropping(
158+
frame.getBuffer().getWidth(),
159+
frame.getBuffer().getHeight(),
160+
() -> internalEncoder.encode(frame, encodeInfo)
161+
);
162+
} else {
163+
return result;
164+
}
165+
} catch (Exception e) {
166+
Logging.e(TAG, "internalEncoder.encode() failed", e);
167+
return retryWithoutCropping(
168+
frame.getBuffer().getWidth(),
169+
frame.getBuffer().getHeight(),
170+
() -> internalEncoder.encode(frame, encodeInfo)
171+
);
172+
} finally {
173+
croppedBuffer.release();
174+
}
175+
}
176+
}
177+
178+
@Override
179+
public VideoCodecStatus setRateAllocation(VideoEncoder.BitrateAllocation allocation, int frameRate) {
180+
return internalEncoder.setRateAllocation(allocation, frameRate);
181+
}
182+
183+
@Override
184+
public VideoEncoder.ScalingSettings getScalingSettings() {
185+
return internalEncoder.getScalingSettings();
186+
}
187+
188+
@Override
189+
public String getImplementationName() {
190+
return internalEncoder.getImplementationName();
191+
}
192+
193+
@Override
194+
public long createNativeVideoEncoder() {
195+
return internalEncoder.createNativeVideoEncoder();
196+
}
197+
198+
@Override
199+
public boolean isHardwareEncoder() {
200+
return internalEncoder.isHardwareEncoder();
201+
}
202+
203+
@Override
204+
public VideoCodecStatus setRates(VideoEncoder.RateControlParameters rcParameters) {
205+
return internalEncoder.setRates(rcParameters);
206+
}
207+
208+
@Override
209+
public VideoEncoder.ResolutionBitrateLimits[] getResolutionBitrateLimits() {
210+
return internalEncoder.getResolutionBitrateLimits();
211+
}
212+
213+
@Override
214+
public VideoEncoder.EncoderInfo getEncoderInfo() {
215+
return internalEncoder.getEncoderInfo();
216+
}
217+
}
218+
219+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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 HardwareVideoEncoderWrapperFactory implements VideoEncoderFactory {
24+
25+
private static final String TAG = "HardwareVideoEncoderWrapperFactory";
26+
27+
private final HardwareVideoEncoderFactory factory;
28+
private final int resolutionPixelAlignment;
29+
30+
public HardwareVideoEncoderWrapperFactory(HardwareVideoEncoderFactory factory, int resolutionPixelAlignment) {
31+
this.factory = factory;
32+
this.resolutionPixelAlignment = resolutionPixelAlignment;
33+
if (resolutionPixelAlignment == 0) {
34+
throw new IllegalArgumentException("resolutionPixelAlignment should not be 0");
35+
}
36+
}
37+
38+
@Override
39+
public VideoEncoder createEncoder(VideoCodecInfo videoCodecInfo) {
40+
try {
41+
VideoEncoder encoder = factory.createEncoder(videoCodecInfo);
42+
if (encoder == null) {
43+
return null;
44+
}
45+
return new HardwareVideoEncoderWrapper(encoder, resolutionPixelAlignment);
46+
} catch (Exception e) {
47+
Logging.e(TAG, "createEncoder failed", e);
48+
return null;
49+
}
50+
}
51+
52+
@Override
53+
public VideoCodecInfo[] getSupportedCodecs() {
54+
return factory.getSupportedCodecs();
55+
}
56+
}

0 commit comments

Comments
 (0)