
This is similar to https://webrtc-review.googlesource.com/c/src/+/3620 for iOS. Using the new WebRtcMediaEngineFactory::Create API, the built-in software video codecs are no longer appended to the injected codecs. To be able to use the software codecs, they are exposed as Java classes through SoftwareVideoEncoderFactory etc. There is also a new DefaultVideoEncoderFactory used by AppRTCMobile. This factory tries to use hardware implementations where available, but falls back to using the injected software codecs. The HardwareVideoEncoderFactory is temporarily also falling back on the software codecs in its default configuration in order to maintain backwards compatibility. Bug: webrtc:7925 Change-Id: I3e8c5ed492ccd160aca968986ad217d7978a951c Reviewed-on: https://webrtc-review.googlesource.com/17480 Reviewed-by: Sami Kalliomäki <sakal@webrtc.org> Reviewed-by: Magnus Jedvert <magjed@webrtc.org> Commit-Queue: Anders Carlsson <andersc@webrtc.org> Cr-Commit-Position: refs/heads/master@{#20647}
273 lines
11 KiB
Java
273 lines
11 KiB
Java
/*
|
|
* Copyright 2017 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree. An additional intellectual property rights grant can be found
|
|
* in the file PATENTS. All contributing project authors may
|
|
* be found in the AUTHORS file in the root of the source tree.
|
|
*/
|
|
|
|
package org.webrtc;
|
|
|
|
import static org.webrtc.MediaCodecUtils.EXYNOS_PREFIX;
|
|
import static org.webrtc.MediaCodecUtils.INTEL_PREFIX;
|
|
import static org.webrtc.MediaCodecUtils.QCOM_PREFIX;
|
|
|
|
import android.media.MediaCodecInfo;
|
|
import android.media.MediaCodecList;
|
|
import android.os.Build;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/** Factory for android hardware video encoders. */
|
|
@SuppressWarnings("deprecation") // API 16 requires the use of deprecated methods.
|
|
public class HardwareVideoEncoderFactory implements VideoEncoderFactory {
|
|
private static final String TAG = "HardwareVideoEncoderFactory";
|
|
|
|
// Forced key frame interval - used to reduce color distortions on Qualcomm platforms.
|
|
private static final int QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_L_MS = 15000;
|
|
private static final int QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_M_MS = 20000;
|
|
private static final int QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_N_MS = 15000;
|
|
|
|
// List of devices with poor H.264 encoder quality.
|
|
// HW H.264 encoder on below devices has poor bitrate control - actual
|
|
// bitrates deviates a lot from the target value.
|
|
private static final List<String> H264_HW_EXCEPTION_MODELS =
|
|
Arrays.asList("SAMSUNG-SGH-I337", "Nexus 7", "Nexus 4");
|
|
|
|
private final EglBase14.Context sharedContext;
|
|
private final boolean enableIntelVp8Encoder;
|
|
private final boolean enableH264HighProfile;
|
|
private final boolean fallbackToSoftware;
|
|
|
|
public HardwareVideoEncoderFactory(
|
|
EglBase.Context sharedContext, boolean enableIntelVp8Encoder, boolean enableH264HighProfile) {
|
|
this(
|
|
sharedContext, enableIntelVp8Encoder, enableH264HighProfile, true /* fallbackToSoftware */);
|
|
}
|
|
|
|
HardwareVideoEncoderFactory(EglBase.Context sharedContext, boolean enableIntelVp8Encoder,
|
|
boolean enableH264HighProfile, boolean fallbackToSoftware) {
|
|
// Texture mode requires EglBase14.
|
|
if (sharedContext instanceof EglBase14.Context) {
|
|
this.sharedContext = (EglBase14.Context) sharedContext;
|
|
} else {
|
|
Logging.w(TAG, "No shared EglBase.Context. Encoders will not use texture mode.");
|
|
this.sharedContext = null;
|
|
}
|
|
this.enableIntelVp8Encoder = enableIntelVp8Encoder;
|
|
this.enableH264HighProfile = enableH264HighProfile;
|
|
this.fallbackToSoftware = fallbackToSoftware;
|
|
}
|
|
|
|
@Deprecated
|
|
public HardwareVideoEncoderFactory(boolean enableIntelVp8Encoder, boolean enableH264HighProfile) {
|
|
this(null, enableIntelVp8Encoder, enableH264HighProfile);
|
|
}
|
|
|
|
@Override
|
|
public VideoEncoder createEncoder(VideoCodecInfo input) {
|
|
VideoCodecType type = VideoCodecType.valueOf(input.name);
|
|
MediaCodecInfo info = findCodecForType(type);
|
|
|
|
if (info == null) {
|
|
// No hardware support for this type.
|
|
// TODO(andersc): This is for backwards compatibility. Remove when clients have migrated to
|
|
// new DefaultVideoEncoderFactory.
|
|
if (fallbackToSoftware) {
|
|
SoftwareVideoEncoderFactory softwareVideoEncoderFactory = new SoftwareVideoEncoderFactory();
|
|
return softwareVideoEncoderFactory.createEncoder(input);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
String codecName = info.getName();
|
|
String mime = type.mimeType();
|
|
Integer surfaceColorFormat = MediaCodecUtils.selectColorFormat(
|
|
MediaCodecUtils.TEXTURE_COLOR_FORMATS, info.getCapabilitiesForType(mime));
|
|
Integer yuvColorFormat = MediaCodecUtils.selectColorFormat(
|
|
MediaCodecUtils.ENCODER_COLOR_FORMATS, info.getCapabilitiesForType(mime));
|
|
|
|
return new HardwareVideoEncoder(codecName, type, surfaceColorFormat, yuvColorFormat,
|
|
input.params, getKeyFrameIntervalSec(type), getForcedKeyFrameIntervalMs(type, codecName),
|
|
createBitrateAdjuster(type, codecName), sharedContext);
|
|
}
|
|
|
|
@Override
|
|
public VideoCodecInfo[] getSupportedCodecs() {
|
|
List<VideoCodecInfo> supportedCodecInfos = new ArrayList<VideoCodecInfo>();
|
|
// Generate a list of supported codecs in order of preference:
|
|
// VP8, VP9, H264 (high profile), and H264 (baseline profile).
|
|
for (VideoCodecType type :
|
|
new VideoCodecType[] {VideoCodecType.VP8, VideoCodecType.VP9, VideoCodecType.H264}) {
|
|
MediaCodecInfo codec = findCodecForType(type);
|
|
if (codec != null) {
|
|
String name = type.name();
|
|
if (type == VideoCodecType.H264 && isH264HighProfileSupported(codec)) {
|
|
supportedCodecInfos.add(new VideoCodecInfo(name, getCodecProperties(type, true)));
|
|
}
|
|
|
|
supportedCodecInfos.add(new VideoCodecInfo(name, getCodecProperties(type, false)));
|
|
}
|
|
}
|
|
|
|
// TODO(andersc): This is for backwards compatibility. Remove when clients have migrated to
|
|
// new DefaultVideoEncoderFactory.
|
|
if (fallbackToSoftware) {
|
|
for (VideoCodecInfo info : SoftwareVideoEncoderFactory.supportedCodecs()) {
|
|
if (!supportedCodecInfos.contains(info)) {
|
|
supportedCodecInfos.add(info);
|
|
}
|
|
}
|
|
}
|
|
|
|
return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
|
|
}
|
|
|
|
private MediaCodecInfo findCodecForType(VideoCodecType type) {
|
|
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
|
|
MediaCodecInfo info = null;
|
|
try {
|
|
info = MediaCodecList.getCodecInfoAt(i);
|
|
} catch (IllegalArgumentException e) {
|
|
Logging.e(TAG, "Cannot retrieve encoder codec info", e);
|
|
}
|
|
|
|
if (info == null || !info.isEncoder()) {
|
|
continue;
|
|
}
|
|
|
|
if (isSupportedCodec(info, type)) {
|
|
return info;
|
|
}
|
|
}
|
|
return null; // No support for this type.
|
|
}
|
|
|
|
// Returns true if the given MediaCodecInfo indicates a supported encoder for the given type.
|
|
private boolean isSupportedCodec(MediaCodecInfo info, VideoCodecType type) {
|
|
if (!MediaCodecUtils.codecSupportsType(info, type)) {
|
|
return false;
|
|
}
|
|
// Check for a supported color format.
|
|
if (MediaCodecUtils.selectColorFormat(
|
|
MediaCodecUtils.ENCODER_COLOR_FORMATS, info.getCapabilitiesForType(type.mimeType()))
|
|
== null) {
|
|
return false;
|
|
}
|
|
return isHardwareSupportedInCurrentSdk(info, type);
|
|
}
|
|
|
|
// Returns true if the given MediaCodecInfo indicates a hardware module that is supported on the
|
|
// current SDK.
|
|
private boolean isHardwareSupportedInCurrentSdk(MediaCodecInfo info, VideoCodecType type) {
|
|
switch (type) {
|
|
case VP8:
|
|
return isHardwareSupportedInCurrentSdkVp8(info);
|
|
case VP9:
|
|
return isHardwareSupportedInCurrentSdkVp9(info);
|
|
case H264:
|
|
return isHardwareSupportedInCurrentSdkH264(info);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean isHardwareSupportedInCurrentSdkVp8(MediaCodecInfo info) {
|
|
String name = info.getName();
|
|
// QCOM Vp8 encoder is supported in KITKAT or later.
|
|
return (name.startsWith(QCOM_PREFIX) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
// Exynos VP8 encoder is supported in M or later.
|
|
|| (name.startsWith(EXYNOS_PREFIX) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
|
// Intel Vp8 encoder is supported in LOLLIPOP or later, with the intel encoder enabled.
|
|
|| (name.startsWith(INTEL_PREFIX) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
|
|
&& enableIntelVp8Encoder);
|
|
}
|
|
|
|
private boolean isHardwareSupportedInCurrentSdkVp9(MediaCodecInfo info) {
|
|
String name = info.getName();
|
|
return (name.startsWith(QCOM_PREFIX) || name.startsWith(EXYNOS_PREFIX))
|
|
// Both QCOM and Exynos VP9 encoders are supported in N or later.
|
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
|
}
|
|
|
|
private boolean isHardwareSupportedInCurrentSdkH264(MediaCodecInfo info) {
|
|
// First, H264 hardware might perform poorly on this model.
|
|
if (H264_HW_EXCEPTION_MODELS.contains(Build.MODEL)) {
|
|
return false;
|
|
}
|
|
String name = info.getName();
|
|
// QCOM H264 encoder is supported in KITKAT or later.
|
|
return (name.startsWith(QCOM_PREFIX) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
|
|
// Exynos H264 encoder is supported in LOLLIPOP or later.
|
|
|| (name.startsWith(EXYNOS_PREFIX)
|
|
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
|
|
}
|
|
|
|
private int getKeyFrameIntervalSec(VideoCodecType type) {
|
|
switch (type) {
|
|
case VP8: // Fallthrough intended.
|
|
case VP9:
|
|
return 100;
|
|
case H264:
|
|
return 20;
|
|
}
|
|
throw new IllegalArgumentException("Unsupported VideoCodecType " + type);
|
|
}
|
|
|
|
private int getForcedKeyFrameIntervalMs(VideoCodecType type, String codecName) {
|
|
if (type == VideoCodecType.VP8 && codecName.startsWith(QCOM_PREFIX)) {
|
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP
|
|
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
return QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_L_MS;
|
|
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
|
|
return QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_M_MS;
|
|
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
|
return QCOM_VP8_KEY_FRAME_INTERVAL_ANDROID_N_MS;
|
|
}
|
|
}
|
|
// Other codecs don't need key frame forcing.
|
|
return 0;
|
|
}
|
|
|
|
private BitrateAdjuster createBitrateAdjuster(VideoCodecType type, String codecName) {
|
|
if (codecName.startsWith(EXYNOS_PREFIX)) {
|
|
if (type == VideoCodecType.VP8) {
|
|
// Exynos VP8 encoders need dynamic bitrate adjustment.
|
|
return new DynamicBitrateAdjuster();
|
|
} else {
|
|
// Exynos VP9 and H264 encoders need framerate-based bitrate adjustment.
|
|
return new FramerateBitrateAdjuster();
|
|
}
|
|
}
|
|
// Other codecs don't need bitrate adjustment.
|
|
return new BaseBitrateAdjuster();
|
|
}
|
|
|
|
private boolean isH264HighProfileSupported(MediaCodecInfo info) {
|
|
return enableH264HighProfile && info.getName().startsWith(QCOM_PREFIX);
|
|
}
|
|
|
|
private Map<String, String> getCodecProperties(VideoCodecType type, boolean highProfile) {
|
|
switch (type) {
|
|
case VP8:
|
|
case VP9:
|
|
return new HashMap<String, String>();
|
|
case H264:
|
|
Map<String, String> properties = new HashMap<>();
|
|
properties.put(VideoCodecInfo.H264_FMTP_LEVEL_ASYMMETRY_ALLOWED, "1");
|
|
properties.put(VideoCodecInfo.H264_FMTP_PACKETIZATION_MODE, "1");
|
|
properties.put(VideoCodecInfo.H264_FMTP_PROFILE_LEVEL_ID,
|
|
highProfile ? VideoCodecInfo.H264_CONSTRAINED_HIGH_3_1
|
|
: VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1);
|
|
return properties;
|
|
default:
|
|
throw new IllegalArgumentException("Unsupported codec: " + type);
|
|
}
|
|
}
|
|
}
|