diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java index c5ef9084..9b32b03c 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/internal/DeviceEncodersTest.java @@ -6,11 +6,8 @@ import android.media.MediaCodecInfo; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.MediumTest; -import androidx.test.rule.ActivityTestRule; import com.otaliastudios.cameraview.BaseTest; -import com.otaliastudios.cameraview.TestActivity; -import com.otaliastudios.cameraview.controls.Grid; import com.otaliastudios.cameraview.size.AspectRatio; import com.otaliastudios.cameraview.size.Size; @@ -30,8 +27,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; @RunWith(AndroidJUnit4.class) @MediumTest @@ -87,7 +82,7 @@ public class DeviceEncodersTest extends BaseTest { if (DeviceEncoders.ENABLED) { List allEncoders = deviceEncoders.getDeviceEncoders(); MediaCodecInfo encoder = deviceEncoders.findDeviceEncoder(allEncoders, - "video/avc", DeviceEncoders.MODE_TAKE_FIRST); + "video/avc", DeviceEncoders.MODE_TAKE_FIRST, 0); assertNotNull(encoder); List encoderTypes = Arrays.asList(encoder.getSupportedTypes()); assertTrue(encoderTypes.contains("video/avc")); 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 d0896e46..1c6446b5 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/internal/DeviceEncoders.java @@ -59,6 +59,26 @@ public class DeviceEncoders { public final static int MODE_TAKE_FIRST = 0; public final static int MODE_PREFER_HARDWARE = 1; + /** + * Exception thrown when trying to find appropriate values + * for a video encoder. + */ + public class VideoException extends RuntimeException { + private VideoException(@NonNull String message) { + super(message); + } + } + + /** + * Exception thrown when trying to find appropriate values + * for an audio encoder. Currently never thrown. + */ + public class AudioException extends RuntimeException { + private AudioException(@NonNull String message) { + super(message); + } + } + @SuppressWarnings("FieldCanBeLocal") private final MediaCodecInfo mVideoEncoder; @SuppressWarnings("FieldCanBeLocal") @@ -68,13 +88,22 @@ public class DeviceEncoders { @SuppressLint("NewApi") public DeviceEncoders(@NonNull String videoType, @NonNull String audioType, int mode) { + this(videoType, audioType, mode, 0, 0); + } + + @SuppressLint("NewApi") + public DeviceEncoders(@NonNull String videoType, + @NonNull String audioType, + int mode, + int videoOffset, + int audioOffset) { // 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 (ENABLED) { List encoders = getDeviceEncoders(); - mVideoEncoder = findDeviceEncoder(encoders, videoType, mode); + mVideoEncoder = findDeviceEncoder(encoders, videoType, mode, videoOffset); LOG.i("Enabled. Found video encoder:", mVideoEncoder.getName()); - mAudioEncoder = findDeviceEncoder(encoders, audioType, mode); + mAudioEncoder = findDeviceEncoder(encoders, audioType, mode, audioOffset); LOG.i("Enabled. Found audio encoder:", mAudioEncoder.getName()); mVideoCapabilities = mVideoEncoder.getCapabilitiesForType(videoType) .getVideoCapabilities(); @@ -139,7 +168,8 @@ public class DeviceEncoders { @VisibleForTesting MediaCodecInfo findDeviceEncoder(@NonNull List encoders, @NonNull String mimeType, - int mode) { + int mode, + int offset) { ArrayList results = new ArrayList<>(); for (MediaCodecInfo encoder : encoders) { String[] types = encoder.getSupportedTypes(); @@ -164,10 +194,12 @@ public class DeviceEncoders { } }); } - if (results.isEmpty()) { + if (results.size() < offset + 1) { + // This should not be a VideoException or AudioException - we want the process + // to crash here. throw new RuntimeException("No encoders for type:" + mimeType); } - return results.get(0); + return results.get(offset); } /** @@ -203,19 +235,19 @@ public class DeviceEncoders { // It's still possible that we're BELOW the lower. if (!mVideoCapabilities.getSupportedWidths().contains(width)) { - throw new RuntimeException("Width not supported after adjustment." + + throw new VideoException("Width not supported after adjustment." + " Desired:" + width + " Range:" + mVideoCapabilities.getSupportedWidths()); } if (!mVideoCapabilities.getSupportedHeights().contains(height)) { - throw new RuntimeException("Height not supported after adjustment." + + throw new VideoException("Height not supported after adjustment." + " Desired:" + height + " Range:" + mVideoCapabilities.getSupportedHeights()); } // It's still possible that we're unsupported for other reasons. if (!mVideoCapabilities.isSizeSupported(width, height)) { - throw new RuntimeException("Size not supported for unknown reason." + + throw new VideoException("Size not supported for unknown reason." + " Might be an aspect ratio issue." + " Desired size:" + new Size(width, height)); } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java index e90c7d9e..ef624e37 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java @@ -101,14 +101,35 @@ public abstract class FullVideoRecorder extends VideoRecorder { } // Check DeviceEncoders support - DeviceEncoders encoders = new DeviceEncoders(videoType, audioType, - DeviceEncoders.MODE_TAKE_FIRST); boolean flip = stub.rotation % 180 != 0; if (flip) stub.size = stub.size.flip(); - stub.size = encoders.getSupportedVideoSize(stub.size); - stub.videoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); - stub.audioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); - stub.videoFrameRate = encoders.getSupportedVideoFrameRate(stub.size, stub.videoFrameRate); + Size newVideoSize = null; + int newVideoBitRate = 0; + int newAudioBitRate = 0; + int newVideoFrameRate = 0; + int videoEncoderOffset = 0; + int audioEncoderOffset = 0; + boolean encodersFound = false; + while (!encodersFound) { + DeviceEncoders encoders = new DeviceEncoders(videoType, audioType, + DeviceEncoders.MODE_TAKE_FIRST, videoEncoderOffset, audioEncoderOffset); + try { + newVideoSize = encoders.getSupportedVideoSize(stub.size); + newVideoBitRate = encoders.getSupportedVideoBitRate(stub.videoBitRate); + newAudioBitRate = encoders.getSupportedAudioBitRate(stub.audioBitRate); + newVideoFrameRate = encoders.getSupportedVideoFrameRate(newVideoSize, + stub.videoFrameRate); + encodersFound = true; + } catch (DeviceEncoders.VideoException videoException) { + videoEncoderOffset++; + } catch (DeviceEncoders.AudioException audioException) { + audioEncoderOffset++; + } + } + stub.size = newVideoSize; + stub.videoBitRate = newVideoBitRate; + stub.audioBitRate = newAudioBitRate; + stub.videoFrameRate = newVideoFrameRate; if (flip) stub.size = stub.size.flip(); // Set video params 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 6dd72c6f..0536dafa 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java @@ -136,13 +136,35 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram String audioType = "audio/mp4a-latm"; // Check the availability of values - DeviceEncoders deviceEncoders = new DeviceEncoders(videoType, audioType, - DeviceEncoders.MODE_PREFER_HARDWARE); - mResult.size = deviceEncoders.getSupportedVideoSize(mResult.size); - mResult.videoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate); - mResult.audioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate); - mResult.videoFrameRate = deviceEncoders.getSupportedVideoFrameRate(mResult.size, - mResult.videoFrameRate); + Size newVideoSize = null; + int newVideoBitRate = 0; + int newAudioBitRate = 0; + int newVideoFrameRate = 0; + int videoEncoderOffset = 0; + int audioEncoderOffset = 0; + boolean encodersFound = false; + DeviceEncoders deviceEncoders = null; + while (!encodersFound) { + deviceEncoders = new DeviceEncoders(videoType, audioType, + DeviceEncoders.MODE_PREFER_HARDWARE, videoEncoderOffset, + audioEncoderOffset); + try { + newVideoSize = deviceEncoders.getSupportedVideoSize(mResult.size); + newVideoBitRate = deviceEncoders.getSupportedVideoBitRate(mResult.videoBitRate); + newAudioBitRate = deviceEncoders.getSupportedAudioBitRate(mResult.audioBitRate); + newVideoFrameRate = deviceEncoders.getSupportedVideoFrameRate(newVideoSize, + mResult.videoFrameRate); + encodersFound = true; + } catch (DeviceEncoders.VideoException videoException) { + videoEncoderOffset++; + } catch (DeviceEncoders.AudioException audioException) { + audioEncoderOffset++; + } + } + mResult.size = newVideoSize; + mResult.videoBitRate = newVideoBitRate; + mResult.audioBitRate = newAudioBitRate; + mResult.videoFrameRate = newVideoFrameRate; // Video TextureConfig videoConfig = new TextureConfig();