Skip to content

Commit 307786e

Browse files
authored
Add support for resolution alignment during encoding (#24)
1 parent b33e7bd commit 307786e

File tree

4 files changed

+532
-0
lines changed

4 files changed

+532
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
17+
package org.webrtc
18+
19+
/**
20+
* The main difference with the standard [DefaultAlignedVideoEncoderFactory] is that this fixes
21+
* issues with resolutions that are not aligned (e.g. VP8 requires 16x16 alignment). You can
22+
* set the alignment by setting [resolutionAdjustment]. Internally the resolution during streaming
23+
* will be cropped to comply with the adjustment. Fallback behaviour is the same as with the
24+
* standard [DefaultVideoEncoderFactory] and it will use the SW encoder if HW fails
25+
* or is not available.
26+
*
27+
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf3042072
28+
* e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/SimulcastVideoEnco
29+
* derFactoryWrapper.kt#L18
30+
*/
31+
class DefaultAlignedVideoEncoderFactory(
32+
eglContext: EglBase.Context?,
33+
enableIntelVp8Encoder: Boolean = true,
34+
enableH264HighProfile: Boolean = false,
35+
resolutionAdjustment: ResolutionAdjustment,
36+
) : VideoEncoderFactory {
37+
private val hardwareVideoEncoderFactory: VideoEncoderFactory
38+
private val softwareVideoEncoderFactory: VideoEncoderFactory = SoftwareVideoEncoderFactory()
39+
40+
init {
41+
val defaultFactory =
42+
HardwareVideoEncoderFactory(eglContext, enableIntelVp8Encoder, enableH264HighProfile)
43+
44+
hardwareVideoEncoderFactory = if (resolutionAdjustment == ResolutionAdjustment.NONE) {
45+
defaultFactory
46+
} else {
47+
HardwareVideoEncoderWrapperFactory(defaultFactory, resolutionAdjustment.value)
48+
}
49+
}
50+
51+
override fun createEncoder(info: VideoCodecInfo): VideoEncoder? {
52+
val softwareEncoder: VideoEncoder? = softwareVideoEncoderFactory.createEncoder(info)
53+
val hardwareEncoder: VideoEncoder? = hardwareVideoEncoderFactory.createEncoder(info)
54+
if (hardwareEncoder != null && softwareEncoder != null) {
55+
return VideoEncoderFallback(softwareEncoder, hardwareEncoder)
56+
}
57+
return hardwareEncoder ?: softwareEncoder
58+
}
59+
60+
override fun getSupportedCodecs(): Array<VideoCodecInfo> {
61+
val supportedCodecInfos = LinkedHashSet<VideoCodecInfo>()
62+
supportedCodecInfos.addAll(listOf(*softwareVideoEncoderFactory.supportedCodecs))
63+
supportedCodecInfos.addAll(listOf(*hardwareVideoEncoderFactory.supportedCodecs))
64+
return supportedCodecInfos.toTypedArray()
65+
}
66+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
17+
package org.webrtc
18+
19+
/**
20+
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf304207
21+
* 2e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/HardwareVideoEnco
22+
* derWrapperFactory.kt
23+
*/
24+
internal class HardwareVideoEncoderWrapper(
25+
private val internalEncoder: VideoEncoder,
26+
private val alignment: Int,
27+
) : VideoEncoder {
28+
class CropSizeCalculator(
29+
alignment: Int,
30+
private val originalWidth: Int,
31+
private val originalHeight: Int,
32+
) {
33+
34+
companion object {
35+
val TAG = CropSizeCalculator::class.simpleName
36+
}
37+
38+
val cropX: Int = originalWidth % alignment
39+
val cropY: Int = originalHeight % alignment
40+
41+
val croppedWidth: Int
42+
get() = originalWidth - cropX
43+
44+
val croppedHeight: Int
45+
get() = originalHeight - cropY
46+
47+
val isCropRequired: Boolean
48+
get() = cropX != 0 || cropY != 0
49+
50+
init {
51+
if (originalWidth != 0 && originalHeight != 0) {
52+
Logging.v(
53+
TAG,
54+
"$this init(): alignment=$alignment" +
55+
"" +
56+
" size=${originalWidth}x$originalHeight => ${croppedWidth}x$croppedHeight",
57+
)
58+
}
59+
}
60+
61+
fun hasFrameSizeChanged(nextWidth: Int, nextHeight: Int): Boolean {
62+
return if (originalWidth == nextWidth && originalHeight == nextHeight) {
63+
false
64+
} else {
65+
Logging.v(
66+
TAG,
67+
"frame size has changed: " +
68+
"${originalWidth}x$originalHeight => ${nextWidth}x$nextHeight",
69+
)
70+
true
71+
}
72+
}
73+
}
74+
75+
companion object {
76+
val TAG = HardwareVideoEncoderWrapper::class.simpleName
77+
}
78+
79+
private var calculator = CropSizeCalculator(1, 0, 0)
80+
81+
private fun retryWithoutCropping(
82+
width: Int,
83+
height: Int,
84+
retryFunc: () -> VideoCodecStatus,
85+
): VideoCodecStatus {
86+
Logging.v(TAG, "retrying without resolution adjustment")
87+
88+
calculator = CropSizeCalculator(1, width, height)
89+
90+
return retryFunc()
91+
}
92+
93+
override fun initEncode(
94+
originalSettings: VideoEncoder.Settings,
95+
callback: VideoEncoder.Callback?,
96+
): VideoCodecStatus {
97+
calculator = CropSizeCalculator(alignment, originalSettings.width, originalSettings.height)
98+
99+
if (!calculator.isCropRequired) {
100+
return internalEncoder.initEncode(originalSettings, callback)
101+
} else {
102+
val croppedSettings = VideoEncoder.Settings(
103+
originalSettings.numberOfCores,
104+
calculator.croppedWidth,
105+
calculator.croppedHeight,
106+
originalSettings.startBitrate,
107+
originalSettings.maxFramerate,
108+
originalSettings.numberOfSimulcastStreams,
109+
originalSettings.automaticResizeOn,
110+
originalSettings.capabilities,
111+
)
112+
113+
try {
114+
val result = internalEncoder.initEncode(croppedSettings, callback)
115+
return if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
116+
Logging.e(
117+
TAG,
118+
"internalEncoder.initEncode() returned FALLBACK_SOFTWARE: " +
119+
"croppedSettings $croppedSettings",
120+
)
121+
retryWithoutCropping(
122+
originalSettings.width,
123+
originalSettings.height,
124+
) { internalEncoder.initEncode(originalSettings, callback) }
125+
} else {
126+
result
127+
}
128+
} catch (e: Exception) {
129+
Logging.e(TAG, "internalEncoder.initEncode() failed", e)
130+
return retryWithoutCropping(
131+
originalSettings.width,
132+
originalSettings.height,
133+
) { internalEncoder.initEncode(originalSettings, callback) }
134+
}
135+
}
136+
}
137+
138+
override fun release(): VideoCodecStatus {
139+
return internalEncoder.release()
140+
}
141+
142+
override fun encode(frame: VideoFrame, encodeInfo: VideoEncoder.EncodeInfo?): VideoCodecStatus {
143+
if (calculator.hasFrameSizeChanged(frame.buffer.width, frame.buffer.height)) {
144+
calculator = CropSizeCalculator(alignment, frame.buffer.width, frame.buffer.height)
145+
}
146+
147+
if (!calculator.isCropRequired) {
148+
return internalEncoder.encode(frame, encodeInfo)
149+
} else {
150+
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/sdk/android/api/org/webrtc/JavaI420Buffer.java;l=172-185;drc=02334e07c5c04c729dd3a8a279bb1fbe24ee8b7c
151+
val croppedWidth = calculator.croppedWidth
152+
val croppedHeight = calculator.croppedHeight
153+
val croppedBuffer = frame.buffer.cropAndScale(
154+
calculator.cropX / 2,
155+
calculator.cropY / 2,
156+
croppedWidth,
157+
croppedHeight,
158+
croppedWidth,
159+
croppedHeight,
160+
)
161+
162+
val croppedFrame = VideoFrame(croppedBuffer, frame.rotation, frame.timestampNs)
163+
164+
try {
165+
val result = internalEncoder.encode(croppedFrame, encodeInfo)
166+
return if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
167+
Logging.e(TAG, "internalEncoder.encode() returned FALLBACK_SOFTWARE")
168+
retryWithoutCropping(frame.buffer.width, frame.buffer.height) {
169+
internalEncoder.encode(
170+
frame,
171+
encodeInfo,
172+
)
173+
}
174+
} else {
175+
result
176+
}
177+
} catch (e: Exception) {
178+
Logging.e(TAG, "internalEncoder.encode() failed", e)
179+
return retryWithoutCropping(
180+
frame.buffer.width,
181+
frame.buffer.height,
182+
) { internalEncoder.encode(frame, encodeInfo) }
183+
} finally {
184+
croppedBuffer.release()
185+
}
186+
}
187+
}
188+
189+
override fun setRateAllocation(
190+
allocation: VideoEncoder.BitrateAllocation?,
191+
frameRate: Int,
192+
): VideoCodecStatus {
193+
return internalEncoder.setRateAllocation(allocation, frameRate)
194+
}
195+
196+
override fun getScalingSettings(): VideoEncoder.ScalingSettings {
197+
return internalEncoder.scalingSettings
198+
}
199+
200+
override fun getImplementationName(): String {
201+
return internalEncoder.implementationName
202+
}
203+
204+
override fun createNativeVideoEncoder(): Long {
205+
return internalEncoder.createNativeVideoEncoder()
206+
}
207+
208+
override fun isHardwareEncoder(): Boolean {
209+
return internalEncoder.isHardwareEncoder
210+
}
211+
212+
override fun setRates(rcParameters: VideoEncoder.RateControlParameters?): VideoCodecStatus {
213+
return internalEncoder.setRates(rcParameters)
214+
}
215+
216+
override fun getResolutionBitrateLimits(): Array<VideoEncoder.ResolutionBitrateLimits> {
217+
return internalEncoder.resolutionBitrateLimits
218+
}
219+
220+
override fun getEncoderInfo(): VideoEncoder.EncoderInfo {
221+
return internalEncoder.encoderInfo
222+
}
223+
}
224+
225+
internal class HardwareVideoEncoderWrapperFactory(
226+
private val factory: HardwareVideoEncoderFactory,
227+
private val resolutionPixelAlignment: Int,
228+
) : VideoEncoderFactory {
229+
companion object {
230+
val TAG = HardwareVideoEncoderWrapperFactory::class.simpleName
231+
}
232+
233+
init {
234+
if (resolutionPixelAlignment == 0) {
235+
throw java.lang.Exception("resolutionPixelAlignment should not be 0")
236+
}
237+
}
238+
239+
override fun createEncoder(videoCodecInfo: VideoCodecInfo?): VideoEncoder? {
240+
try {
241+
val encoder = factory.createEncoder(videoCodecInfo) ?: return null
242+
return HardwareVideoEncoderWrapper(encoder, resolutionPixelAlignment)
243+
} catch (e: Exception) {
244+
Logging.e(TAG, "createEncoder failed", e)
245+
return null
246+
}
247+
}
248+
249+
override fun getSupportedCodecs(): Array<VideoCodecInfo> {
250+
return factory.supportedCodecs
251+
}
252+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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+
17+
package org.webrtc
18+
19+
/**
20+
* Resolution alignment values. Generally the [MULTIPLE_OF_16] is recommended
21+
* for both VP8 and H264
22+
*/
23+
enum class ResolutionAdjustment(val value: Int) {
24+
NONE(1),
25+
MULTIPLE_OF_2(2),
26+
MULTIPLE_OF_4(4),
27+
MULTIPLE_OF_8(8),
28+
MULTIPLE_OF_16(16),
29+
}

0 commit comments

Comments
 (0)