diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java index 015d5ff3..7baf7b69 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.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 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 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 getDeviceEncoders() { - if (Build.VERSION.SDK_INT >= 21) { - ArrayList 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; + ArrayList results = new ArrayList<>(); + MediaCodecInfo[] array = new MediaCodecList(MediaCodecList.REGULAR_CODECS).getCodecInfos(); + for (MediaCodecInfo info : array) { + if (info.isEncoder()) results.add(info); } + return results; } + /** + * 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") - @Nullable - private List getCapabilities(@NonNull String mimeType) { - if (mEncoders == null) return null; - ArrayList results = new ArrayList<>(); - for (MediaCodecInfo encoder : mEncoders) { + 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") + @NonNull + private MediaCodecInfo findDeviceEncoder(@NonNull List encoders, @NonNull String mimeType) { + ArrayList 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() { + @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") - public boolean supportsVideo(@NonNull String mimeType, int width, int height, int bitRate) { - List 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; - } + @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()); } - return false; + 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 supportsAudio(@NonNull String mimeType, int sampleRate, int bitRate) { - List 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; - } + 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; + } + } + + /** + * Returns the name of the audio encoder if we were able to determine one. + * @return encoder name + */ + @SuppressLint("NewApi") + @Nullable + public String getAudioEncoder() { + if (mAudioEncoder != null) { + return mAudioEncoder.getName(); + } else { + return null; } - return false; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java index 12c3515e..4a13bc5b 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -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); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java index fb663566..6372eee7 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioConfig.java @@ -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; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java index 9f03f97e..fb99b596 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/AudioMediaEncoder.java @@ -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 { - mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); + if (mConfig.encoder != null) { + mMediaCodec = MediaCodec.createByCodecName(mConfig.encoder); + } else { + mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); + } } catch (IOException e) { throw new RuntimeException(e); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoConfig.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoConfig.java index 5631d808..9a34205e 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoConfig.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoConfig.java @@ -13,6 +13,7 @@ public class VideoConfig { public int frameRate; public int rotation; public String mimeType; + public String encoder; protected 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; } } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java index a70e0342..5b582f49 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/VideoMediaEncoder.java @@ -67,7 +67,11 @@ abstract class VideoMediaEncoder extends MediaEncoder { format.setInteger("rotation-degrees", mConfig.rotation); try { - mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); + if (mConfig.encoder != null) { + mMediaCodec = MediaCodec.createByCodecName(mConfig.encoder); + } else { + mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); + } } catch (IOException e) { throw new RuntimeException(e); }