Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Java) Add support for resolution alignment during encoding #25

Merged
merged 10 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions sdk/android/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ if (is_android) {
"api/org/webrtc/WrappedNativeVideoEncoder.java",
"api/org/webrtc/YuvConverter.java",
"api/org/webrtc/YuvHelper.java",
"api/org/webrtc/ResolutionAdjustment.java",
"src/java/org/webrtc/EglBase10Impl.java",
"src/java/org/webrtc/EglBase14Impl.java",
"src/java/org/webrtc/GlGenericDrawer.java",
Expand Down Expand Up @@ -363,6 +364,8 @@ if (is_android) {
"api/org/webrtc/DefaultVideoDecoderFactory.java",
"api/org/webrtc/DefaultVideoEncoderFactory.java",
"api/org/webrtc/WrappedVideoDecoderFactory.java",
"api/org/webrtc/DefaultAlignedVideoEncoderFactory.java",
"api/org/webrtc/SimulcastAlignedVideoEncoderFactory.java",
]

deps = [
Expand Down Expand Up @@ -394,6 +397,8 @@ if (is_android) {
sources = [
"api/org/webrtc/HardwareVideoDecoderFactory.java",
"api/org/webrtc/HardwareVideoEncoderFactory.java",
"api/org/webrtc/HardwareVideoEncoderWrapper.java",
"api/org/webrtc/HardwareVideoEncoderWrapperFactory.java",
"api/org/webrtc/PlatformSoftwareVideoDecoderFactory.java",
"src/java/org/webrtc/AndroidVideoDecoder.java",
"src/java/org/webrtc/BaseBitrateAdjuster.java",
Expand Down
69 changes: 69 additions & 0 deletions sdk/android/api/org/webrtc/DefaultAlignedVideoEncoderFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2014-2023 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.webrtc;

import java.util.Arrays;
import java.util.LinkedHashSet;

/**
* The main difference with the standard [DefaultAlignedVideoEncoderFactory] is that this fixes
* issues with resolutions that are not aligned (e.g. VP8 requires 16x16 alignment). You can
* set the alignment by setting [resolutionAdjustment]. Internally the resolution during streaming
* will be cropped to comply with the adjustment. Fallback behaviour is the same as with the
* standard [DefaultVideoEncoderFactory] and it will use the SW encoder if HW fails
* or is not available.
*
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf3042072
* e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/SimulcastVideoEnco
* derFactoryWrapper.kt#L18
*/
public class DefaultAlignedVideoEncoderFactory implements VideoEncoderFactory {
private final VideoEncoderFactory hardwareVideoEncoderFactory;
private final VideoEncoderFactory softwareVideoEncoderFactory;

public DefaultAlignedVideoEncoderFactory(
EglBase.Context eglContext,
boolean enableIntelVp8Encoder,
boolean enableH264HighProfile,
ResolutionAdjustment resolutionAdjustment
) {
HardwareVideoEncoderFactory defaultFactory =
new HardwareVideoEncoderFactory(eglContext, enableIntelVp8Encoder, enableH264HighProfile);
hardwareVideoEncoderFactory = (resolutionAdjustment == ResolutionAdjustment.NONE) ?
defaultFactory :
new HardwareVideoEncoderWrapperFactory(defaultFactory, resolutionAdjustment.getValue());
softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
}

@Override
public VideoEncoder createEncoder(VideoCodecInfo info) {
VideoEncoder softwareEncoder = softwareVideoEncoderFactory.createEncoder(info);
VideoEncoder hardwareEncoder = hardwareVideoEncoderFactory.createEncoder(info);
if (hardwareEncoder != null && softwareEncoder != null) {
return new VideoEncoderFallback(softwareEncoder, hardwareEncoder);
}
return hardwareEncoder != null ? hardwareEncoder : softwareEncoder;
}

@Override
public VideoCodecInfo[] getSupportedCodecs() {
LinkedHashSet<VideoCodecInfo> supportedCodecInfos = new LinkedHashSet<>();
supportedCodecInfos.addAll(Arrays.asList(softwareVideoEncoderFactory.getSupportedCodecs()));
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoEncoderFactory.getSupportedCodecs()));
return supportedCodecInfos.toArray(new VideoCodecInfo[0]);
}
}

219 changes: 219 additions & 0 deletions sdk/android/api/org/webrtc/HardwareVideoEncoderWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.webrtc;

/**
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf304207
* 2e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/HardwareVideoEnco
* derWrapperFactory.kt
*/
class HardwareVideoEncoderWrapper implements VideoEncoder {

private static final String TAG = "HardwareVideoEncoderWrapper";

private final VideoEncoder internalEncoder;
private final int alignment;

public HardwareVideoEncoderWrapper(VideoEncoder internalEncoder, int alignment) {
this.internalEncoder = internalEncoder;
this.alignment = alignment;
}

private static class CropSizeCalculator {

private static final String TAG = "CropSizeCalculator";

private final int alignment;
private final int originalWidth;
private final int originalHeight;
private final int cropX;
private final int cropY;

public CropSizeCalculator(int alignment, int originalWidth, int originalHeight) {
this.alignment = alignment;
this.originalWidth = originalWidth;
this.originalHeight = originalHeight;
this.cropX = originalWidth % alignment;
this.cropY = originalHeight % alignment;
if (originalWidth != 0 && originalHeight != 0) {
Logging.v(TAG, "init(): alignment=" + alignment +
" size=" + originalWidth + "x" + originalHeight + " => " + getCroppedWidth() + "x" + getCroppedHeight());
}
}

public int getCroppedWidth() {
return originalWidth - cropX;
}

public int getCroppedHeight() {
return originalHeight - cropY;
}

public boolean isCropRequired() {
return cropX != 0 || cropY != 0;
}

public boolean hasFrameSizeChanged(int nextWidth, int nextHeight) {
if (originalWidth == nextWidth && originalHeight == nextHeight) {
return false;
} else {
Logging.v(TAG, "frame size has changed: " +
originalWidth + "x" + originalHeight + " => " + nextWidth + "x" + nextHeight);
return true;
}
}
}

private CropSizeCalculator calculator = new CropSizeCalculator(1, 0, 0);

private VideoCodecStatus retryWithoutCropping(int width, int height, Runnable retryFunc) {
Logging.v(TAG, "retrying without resolution adjustment");
calculator = new CropSizeCalculator(1, width, height);
retryFunc.run();
return VideoCodecStatus.OK;
}

@Override
public VideoCodecStatus initEncode(VideoEncoder.Settings originalSettings, VideoEncoder.Callback callback) {
calculator = new CropSizeCalculator(alignment, originalSettings.width, originalSettings.height);
if (!calculator.isCropRequired()) {
return internalEncoder.initEncode(originalSettings, callback);
} else {
VideoEncoder.Settings croppedSettings = new VideoEncoder.Settings(
originalSettings.numberOfCores,
calculator.getCroppedWidth(),
calculator.getCroppedHeight(),
originalSettings.startBitrate,
originalSettings.maxFramerate,
originalSettings.numberOfSimulcastStreams,
originalSettings.automaticResizeOn,
originalSettings.capabilities
);
try {
VideoCodecStatus result = internalEncoder.initEncode(croppedSettings, callback);
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
Logging.e(TAG, "internalEncoder.initEncode() returned FALLBACK_SOFTWARE: " +
"croppedSettings " + croppedSettings);
return retryWithoutCropping(
originalSettings.width,
originalSettings.height,
() -> internalEncoder.initEncode(originalSettings, callback)
);
} else {
return result;
}
} catch (Exception e) {
Logging.e(TAG, "internalEncoder.initEncode() failed", e);
return retryWithoutCropping(
originalSettings.width,
originalSettings.height,
() -> internalEncoder.initEncode(originalSettings, callback)
);
}
}
}

@Override
public VideoCodecStatus release() {
return internalEncoder.release();
}

@Override
public VideoCodecStatus encode(VideoFrame frame, VideoEncoder.EncodeInfo encodeInfo) {
if (calculator.hasFrameSizeChanged(frame.getBuffer().getWidth(), frame.getBuffer().getHeight())) {
calculator = new CropSizeCalculator(alignment, frame.getBuffer().getWidth(), frame.getBuffer().getHeight());
}
if (!calculator.isCropRequired()) {
return internalEncoder.encode(frame, encodeInfo);
} else {
int croppedWidth = calculator.getCroppedWidth();
int croppedHeight = calculator.getCroppedHeight();
VideoFrame.Buffer croppedBuffer = frame.getBuffer().cropAndScale(
calculator.cropX / 2,
calculator.cropY / 2,
croppedWidth,
croppedHeight,
croppedWidth,
croppedHeight
);
VideoFrame croppedFrame = new VideoFrame(croppedBuffer, frame.getRotation(), frame.getTimestampNs());
try {
VideoCodecStatus result = internalEncoder.encode(croppedFrame, encodeInfo);
if (result == VideoCodecStatus.FALLBACK_SOFTWARE) {
Logging.e(TAG, "internalEncoder.encode() returned FALLBACK_SOFTWARE");
return retryWithoutCropping(
frame.getBuffer().getWidth(),
frame.getBuffer().getHeight(),
() -> internalEncoder.encode(frame, encodeInfo)
);
} else {
return result;
}
} catch (Exception e) {
Logging.e(TAG, "internalEncoder.encode() failed", e);
return retryWithoutCropping(
frame.getBuffer().getWidth(),
frame.getBuffer().getHeight(),
() -> internalEncoder.encode(frame, encodeInfo)
);
} finally {
croppedBuffer.release();
}
}
}

@Override
public VideoCodecStatus setRateAllocation(VideoEncoder.BitrateAllocation allocation, int frameRate) {
return internalEncoder.setRateAllocation(allocation, frameRate);
}

@Override
public VideoEncoder.ScalingSettings getScalingSettings() {
return internalEncoder.getScalingSettings();
}

@Override
public String getImplementationName() {
return internalEncoder.getImplementationName();
}

@Override
public long createNativeVideoEncoder() {
return internalEncoder.createNativeVideoEncoder();
}

@Override
public boolean isHardwareEncoder() {
return internalEncoder.isHardwareEncoder();
}

@Override
public VideoCodecStatus setRates(VideoEncoder.RateControlParameters rcParameters) {
return internalEncoder.setRates(rcParameters);
}

@Override
public VideoEncoder.ResolutionBitrateLimits[] getResolutionBitrateLimits() {
return internalEncoder.getResolutionBitrateLimits();
}

@Override
public VideoEncoder.EncoderInfo getEncoderInfo() {
return internalEncoder.getEncoderInfo();
}
}


56 changes: 56 additions & 0 deletions sdk/android/api/org/webrtc/HardwareVideoEncoderWrapperFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.webrtc;

/**
* Original source: https://github.com/shiguredo/sora-android-sdk/blob/3cc88e806ab2f2327bf304207
* 2e98d6da9df4408/sora-android-sdk/src/main/kotlin/jp/shiguredo/sora/sdk/codec/HardwareVideoEnco
* derWrapperFactory.kt
*/
class HardwareVideoEncoderWrapperFactory implements VideoEncoderFactory {

private static final String TAG = "HardwareVideoEncoderWrapperFactory";

private final HardwareVideoEncoderFactory factory;
private final int resolutionPixelAlignment;

public HardwareVideoEncoderWrapperFactory(HardwareVideoEncoderFactory factory, int resolutionPixelAlignment) {
this.factory = factory;
this.resolutionPixelAlignment = resolutionPixelAlignment;
if (resolutionPixelAlignment == 0) {
throw new IllegalArgumentException("resolutionPixelAlignment should not be 0");
}
}

@Override
public VideoEncoder createEncoder(VideoCodecInfo videoCodecInfo) {
try {
VideoEncoder encoder = factory.createEncoder(videoCodecInfo);
if (encoder == null) {
return null;
}
return new HardwareVideoEncoderWrapper(encoder, resolutionPixelAlignment);
} catch (Exception e) {
Logging.e(TAG, "createEncoder failed", e);
return null;
}
}

@Override
public VideoCodecInfo[] getSupportedCodecs() {
return factory.getSupportedCodecs();
}
}
Loading