Prefer hardware encoders, adjust size and bitrate

pull/545/head
Mattia Iavarone 6 years ago
parent e5ac17be5d
commit 6fb2c41eb2
  1. 209
      cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java
  2. 34
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  3. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java
  4. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java
  5. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoConfig.java
  6. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java

@ -2,88 +2,219 @@ package com.otaliastudios.cameraview.internal;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.size.Size;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Checks the capabilities of device encoders.
* Checks the capabilities of device encoders and adjust parameters to ensure
* that they'll be supported by the final encoder.
*/
public class DeviceEncoders {
private final List<MediaCodecInfo> mEncoders;
private final static String TAG = DeviceEncoders.class.getSimpleName();
private final static CameraLogger LOG = CameraLogger.create(TAG);
private final boolean mEnabled;
@SuppressWarnings("FieldCanBeLocal")
private final MediaCodecInfo mVideoEncoder;
@SuppressWarnings("FieldCanBeLocal")
private final MediaCodecInfo mAudioEncoder;
private final MediaCodecInfo.VideoCapabilities mVideoCapabilities;
private final MediaCodecInfo.AudioCapabilities mAudioCapabilities;
public DeviceEncoders() {
mEncoders = getDeviceEncoders();
@SuppressLint("NewApi")
public DeviceEncoders(@NonNull String videoType, @NonNull String audioType) {
mEnabled = Build.VERSION.SDK_INT >= 21;
// We could still get a list of MediaCodecInfo for API >= 16, but it seems that the APIs
// for querying the availability of a specified MediaFormat were only added in 21 anyway.
if (mEnabled) {
List<MediaCodecInfo> encoders = getDeviceEncoders();
mVideoEncoder = findDeviceEncoder(encoders, videoType);
LOG.i("Enabled. Found video encoder:", mVideoEncoder.getName());
mAudioEncoder = findDeviceEncoder(encoders, audioType);
LOG.i("Enabled. Found audio encoder:", mAudioEncoder.getName());
mVideoCapabilities = mVideoEncoder.getCapabilitiesForType(videoType).getVideoCapabilities();
mAudioCapabilities = mAudioEncoder.getCapabilitiesForType(audioType).getAudioCapabilities();
} else {
mVideoEncoder = null;
mAudioEncoder = null;
mVideoCapabilities = null;
mAudioCapabilities = null;
LOG.i("Disabled.");
}
}
@Nullable
/**
* Collects all the device encoders, which means excluding decoders.
* @return encoders
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@NonNull
private List<MediaCodecInfo> getDeviceEncoders() {
if (Build.VERSION.SDK_INT >= 21) {
ArrayList<MediaCodecInfo> results = new ArrayList<>();
MediaCodecInfo[] array = new MediaCodecList(MediaCodecList.REGULAR_CODECS).getCodecInfos();
for (MediaCodecInfo info : array) {
if (info.isEncoder()) results.add(info);
}
return results;
} else {
// We could still get a list of MediaCodecInfo for API >= 16, but it seems that the APIs
// for querying the availability of a specified MediaFormat were only added in 21 anyway.
return null;
}
/**
* Whether an encoder is a hardware encoder or not. We don't have an API to check this,
* but it seems that we can assume that all OMX.google encoders are software encoders.
*
* @param encoder encoder
* @return true if hardware
*/
@SuppressLint("NewApi")
private boolean isHardwareEncoder(@NonNull MediaCodecInfo encoder) {
return !encoder.getName().startsWith("OMX.google");
}
/**
* Finds the encoder we'll be using.
* The way we do this is to list all possible encoders and prefer the hardware encoders
* over software encoders. Throws if we find no encoder for this type.
*
* @param mimeType mime type
* @return encoder
*/
@SuppressLint("NewApi")
@Nullable
private List<MediaCodecInfo.CodecCapabilities> getCapabilities(@NonNull String mimeType) {
if (mEncoders == null) return null;
ArrayList<MediaCodecInfo.CodecCapabilities> results = new ArrayList<>();
for (MediaCodecInfo encoder : mEncoders) {
@NonNull
private MediaCodecInfo findDeviceEncoder(@NonNull List<MediaCodecInfo> encoders, @NonNull String mimeType) {
ArrayList<MediaCodecInfo> results = new ArrayList<>();
for (MediaCodecInfo encoder : encoders) {
String[] types = encoder.getSupportedTypes();
for (String type : types) {
if (type.equalsIgnoreCase(mimeType)) {
results.add(encoder.getCapabilitiesForType(type));
results.add(encoder);
break;
}
}
}
return results;
LOG.i("findDeviceEncoder -", "type:", mimeType, "encoders:", results.size());
Collections.sort(results, new Comparator<MediaCodecInfo>() {
@Override
public int compare(MediaCodecInfo o1, MediaCodecInfo o2) {
boolean hw1 = isHardwareEncoder(o1);
boolean hw2 = isHardwareEncoder(o2);
if (hw1 && hw2) return 0;
if (hw1) return 1;
if (hw2) return -1;
return 0;
}
});
if (results.isEmpty()) {
throw new RuntimeException("No encoders for type:" + mimeType);
}
return results.get(0);
}
/**
* Returns a video size supported by the device encoders.
* Throws if input width or height are out of the supported boundaries.
*
* @param size input size
* @return adjusted size
*/
@SuppressLint("NewApi")
@NonNull
public Size getSupportedVideoSize(@NonNull Size size) {
if (!mEnabled) return size;
int width = size.getWidth();
int height = size.getHeight();
while (width % mVideoCapabilities.getWidthAlignment() != 0) width++;
while (height % mVideoCapabilities.getHeightAlignment() != 0) height++;
if (!mVideoCapabilities.getSupportedWidths().contains(width)) {
throw new RuntimeException("Width not supported after adjustment." +
" Desired:" + width +
" Range:" + mVideoCapabilities.getSupportedWidths());
}
if (!mVideoCapabilities.getSupportedHeights().contains(height)) {
throw new RuntimeException("Height not supported after adjustment." +
" Desired:" + height +
" Range:" + mVideoCapabilities.getSupportedHeights());
}
Size adjusted = new Size(width, height);
LOG.i("getSupportedVideoSize -", "inputSize:", size, "adjustedSize:", adjusted);
return adjusted;
}
/**
* Returns a video bit rate supported by the device encoders.
* This means adjusting the input bit rate if needed, to match encoder constraints.
*
* @param bitRate input rate
* @return adjusted rate
*/
@SuppressLint("NewApi")
public boolean supportsVideo(@NonNull String mimeType, int width, int height, int bitRate) {
List<MediaCodecInfo.CodecCapabilities> capabilities = getCapabilities(mimeType);
if (capabilities == null) return true;
if (capabilities.isEmpty()) return false;
for (MediaCodecInfo.CodecCapabilities cap : capabilities) {
MediaCodecInfo.VideoCapabilities videoCap = cap.getVideoCapabilities();
if (videoCap.isSizeSupported(width, height)
&& videoCap.getBitrateRange().contains(bitRate)) {
return true;
public int getSupportedVideoBitRate(int bitRate) {
if (!mEnabled) return bitRate;
int newBitRate = mVideoCapabilities.getBitrateRange().clamp(bitRate);
LOG.i("getSupportedVideoBitRate -", "inputRate:", bitRate, "adjustedRate:", newBitRate);
return newBitRate;
}
/**
* Returns an audio bit rate supported by the device encoders.
* This means adjusting the input bit rate if needed, to match encoder constraints.
*
* @param bitRate input rate
* @return adjusted rate
*/
@SuppressLint("NewApi")
public int getSupportedAudioBitRate(int bitRate) {
if (!mEnabled) return bitRate;
int newBitRate = mAudioCapabilities.getBitrateRange().clamp(bitRate);
LOG.i("getSupportedAudioBitRate -", "inputRate:", bitRate, "adjustedRate:", newBitRate);
return newBitRate;
}
// Won't do this for audio sample rate. As far as I remember, the value we're using,
// 44.1kHz, is guaranteed to be available, and it's not configurable.
/**
* Returns the name of the video encoder if we were able to determine one.
* @return encoder name
*/
@SuppressLint("NewApi")
@Nullable
public String getVideoEncoder() {
if (mVideoEncoder != null) {
return mVideoEncoder.getName();
} else {
return null;
}
return false;
}
/**
* Returns the name of the audio encoder if we were able to determine one.
* @return encoder name
*/
@SuppressLint("NewApi")
public boolean supportsAudio(@NonNull String mimeType, int sampleRate, int bitRate) {
List<MediaCodecInfo.CodecCapabilities> capabilities = getCapabilities(mimeType);
if (capabilities == null) return true;
if (capabilities.isEmpty()) return false;
for (MediaCodecInfo.CodecCapabilities cap : capabilities) {
MediaCodecInfo.AudioCapabilities audioCap = cap.getAudioCapabilities();
if (audioCap.isSampleRateSupported(sampleRate)
&& audioCap.getBitrateRange().contains(bitRate)) {
return true;
}
}
return false;
@Nullable
public String getAudioEncoder() {
if (mAudioEncoder != null) {
return mAudioEncoder.getName();
} else {
return null;
}
}
}

@ -5,6 +5,7 @@ import android.opengl.EGL14;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
@ -121,26 +122,30 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
if (mResult.videoBitRate <= 0) mResult.videoBitRate = estimateVideoBitRate(mResult.size, mResult.videoFrameRate);
if (mResult.audioBitRate <= 0) mResult.audioBitRate = DEFAULT_AUDIO_BITRATE;
// Video. Ensure width and height are divisible by 2, as I have read somewhere.
Size size = mResult.size;
int width = size.getWidth();
int height = size.getHeight();
width = width % 2 == 0 ? width : width + 1;
height = height % 2 == 0 ? height : height + 1;
String type = "";
// Define mime types
String videoType = "";
switch (mResult.videoCodec) {
case H_263: type = "video/3gpp"; break; // MediaFormat.MIMETYPE_VIDEO_H263;
case H_264: type = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: type = "video/avc"; break;
case H_263: videoType = "video/3gpp"; break; // MediaFormat.MIMETYPE_VIDEO_H263;
case H_264: videoType = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: videoType = "video/avc"; break;
}
LOG.w("Creating frame encoder. Rotation:", mResult.rotation);
String audioType = "audio/mp4a-latm";
// Check the availability of values
DeviceEncoders deviceEncoders = new DeviceEncoders(videoType, audioType);
mResult.size = deviceEncoders.getSupportedVideoSize(mResult.size);
mResult.videoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate);
mResult.audioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate);
// Video
TextureConfig videoConfig = new TextureConfig();
videoConfig.width = width;
videoConfig.height = height;
videoConfig.width = mResult.size.getWidth();
videoConfig.height = mResult.size.getHeight();
videoConfig.bitRate = mResult.videoBitRate;
videoConfig.frameRate = mResult.videoFrameRate;
videoConfig.rotation = mResult.rotation;
videoConfig.mimeType = type;
videoConfig.mimeType = videoType;
videoConfig.encoder = deviceEncoders.getVideoEncoder();
videoConfig.textureId = mTextureId;
videoConfig.scaleX = scaleX;
videoConfig.scaleY = scaleY;
@ -162,6 +167,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
audioConfig.bitRate = mResult.audioBitRate;
if (mResult.audio == Audio.MONO) audioConfig.channels = 1;
if (mResult.audio == Audio.STEREO) audioConfig.channels = 2;
audioConfig.encoder = deviceEncoders.getAudioEncoder();
audioEncoder = new AudioMediaEncoder(audioConfig);
}

@ -14,9 +14,10 @@ public class AudioConfig {
// Configurable options
public int bitRate; // ENCODED bit rate
public int channels = 1;
public String encoder;
public String mimeType = "audio/mp4a-latm";
// Not configurable options (for now)
final String mimeType = "audio/mp4a-latm";
final int encoding = AudioFormat.ENCODING_PCM_16BIT; // Determines the sampleSizePerChannel
// The 44.1KHz frequency is the only setting guaranteed to be available on all devices.
final int samplingFrequency = 44100; // samples/sec
@ -28,6 +29,8 @@ public class AudioConfig {
AudioConfig config = new AudioConfig();
config.bitRate = this.bitRate;
config.channels = this.channels;
config.encoder = this.encoder;
config.mimeType = mimeType;
return config;
}

@ -71,7 +71,11 @@ public class AudioMediaEncoder extends MediaEncoder {
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, mConfig.audioFormatChannels());
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate);
try {
if (mConfig.encoder != null) {
mMediaCodec = MediaCodec.createByCodecName(mConfig.encoder);
} else {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

@ -13,6 +13,7 @@ public class VideoConfig {
public int frameRate;
public int rotation;
public String mimeType;
public String encoder;
protected <C extends VideoConfig> void copy(@NonNull C output) {
output.width = this.width;
@ -21,5 +22,6 @@ public class VideoConfig {
output.frameRate = this.frameRate;
output.rotation = this.rotation;
output.mimeType = this.mimeType;
output.encoder = this.encoder;
}
}

@ -67,7 +67,11 @@ abstract class VideoMediaEncoder<C extends VideoConfig> extends MediaEncoder {
format.setInteger("rotation-degrees", mConfig.rotation);
try {
if (mConfig.encoder != null) {
mMediaCodec = MediaCodec.createByCodecName(mConfig.encoder);
} else {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType);
}
} catch (IOException e) {
throw new RuntimeException(e);
}

Loading…
Cancel
Save