Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Commit

Permalink
[camera] Fix CamcorderProfile Usages (#4423)
Browse files Browse the repository at this point in the history
  • Loading branch information
camsim99 committed Oct 29, 2021
1 parent 7588dd9 commit 29f46b4
Show file tree
Hide file tree
Showing 13 changed files with 659 additions and 67 deletions.
3 changes: 3 additions & 0 deletions packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## 0.9.4+3

* Change Android compileSdkVersion to 31.
* Remove usages of deprecated Android API `CamcorderProfile`.
* Update gradle version to 7.0.2 on Android.
* Fix registerTexture and result being called on background thread on iOS.

## 0.9.4+2
Expand Down
9 changes: 5 additions & 4 deletions packages/camera/camera/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildscript {
}

dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:7.0.2'
}
}

Expand All @@ -27,9 +27,10 @@ project.getTasks().withType(JavaCompile){
apply plugin: 'com.android.library'

android {
compileSdkVersion 29
compileSdkVersion 31

defaultConfig {
targetSdkVersion 31
minSdkVersion 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down Expand Up @@ -60,7 +61,7 @@ android {
dependencies {
compileOnly 'androidx.annotation:annotation:1.1.0'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-inline:3.12.4'
testImplementation 'org.mockito:mockito-inline:4.0.0'
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'org.robolectric:robolectric:4.5'
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.media.CamcorderProfile;
import android.media.EncoderProfiles;
import android.media.Image;
import android.media.ImageReader;
import android.media.MediaRecorder;
Expand Down Expand Up @@ -199,8 +200,16 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
((SensorOrientationFeature) cameraFeatures.getSensorOrientation())
.getLockedCaptureOrientation();

MediaRecorderBuilder mediaRecorderBuilder;

if (Build.VERSION.SDK_INT >= 31) {
mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath);
} else {
mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath);
}

mediaRecorder =
new MediaRecorderBuilder(getRecordingProfile(), outputFilePath)
mediaRecorderBuilder
.setEnableAudio(enableAudio)
.setMediaOrientation(
lockedOrientation == null
Expand Down Expand Up @@ -918,8 +927,12 @@ public float getMinZoomLevel() {
return cameraFeatures.getZoomLevel().getMinimumZoomLevel();
}

/** Shortcut to get current recording profile. */
CamcorderProfile getRecordingProfile() {
/** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */
CamcorderProfile getRecordingProfileLegacy() {
return cameraFeatures.getResolution().getRecordingProfileLegacy();
}

EncoderProfiles getRecordingProfile() {
return cameraFeatures.getResolution().getRecordingProfile();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

package io.flutter.plugins.camera.features.resolution;

import android.annotation.TargetApi;
import android.hardware.camera2.CaptureRequest;
import android.media.CamcorderProfile;
import android.media.EncoderProfiles;
import android.os.Build;
import android.util.Size;
import androidx.annotation.VisibleForTesting;
import io.flutter.plugins.camera.CameraProperties;
import io.flutter.plugins.camera.features.CameraFeature;
import java.util.List;

/**
* Controls the resolutions configuration on the {@link android.hardware.camera2} API.
Expand All @@ -21,7 +25,8 @@
public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
private Size captureSize;
private Size previewSize;
private CamcorderProfile recordingProfile;
private CamcorderProfile recordingProfileLegacy;
private EncoderProfiles recordingProfile;
private ResolutionPreset currentSetting;
private int cameraId;

Expand Down Expand Up @@ -51,7 +56,11 @@ public ResolutionFeature(
*
* @return Resolution information to configure the {@link android.hardware.camera2} API.
*/
public CamcorderProfile getRecordingProfile() {
public CamcorderProfile getRecordingProfileLegacy() {
return this.recordingProfileLegacy;
}

public EncoderProfiles getRecordingProfile() {
return this.recordingProfile;
}

Expand Down Expand Up @@ -100,19 +109,29 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
}

@VisibleForTesting
static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) {
static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset)
throws IndexOutOfBoundsException {
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
preset = ResolutionPreset.high;
}
if (Build.VERSION.SDK_INT >= 31) {
EncoderProfiles profile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
List<EncoderProfiles.VideoProfile> videoProfiles = profile.getVideoProfiles();
EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);

CamcorderProfile profile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
} else {
@SuppressWarnings("deprecation")
CamcorderProfile profile =
getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset);
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
}
}

/**
* Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link
* ResolutionPreset}.
* ResolutionPreset}. Supports SDK < 31.
*
* @param cameraId Camera identifier which indicates the device's camera for which to select a
* {@link android.media.CamcorderProfile}.
Expand All @@ -121,7 +140,7 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) {
* @return The best possible {@link android.media.CamcorderProfile} that matches the supplied
* {@link ResolutionPreset}.
*/
public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy(
int cameraId, ResolutionPreset preset) {
if (cameraId < 0) {
throw new AssertionError(
Expand Down Expand Up @@ -164,13 +183,74 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres
}
}

private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) {
@TargetApi(Build.VERSION_CODES.S)
public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset(
int cameraId, ResolutionPreset preset) {
if (cameraId < 0) {
throw new AssertionError(
"getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers.");
}

String cameraIdString = Integer.toString(cameraId);

switch (preset) {
// All of these cases deliberately fall through to get the best available profile.
case max:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH);
}
case ultraHigh:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P);
}
case veryHigh:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P);
}
case high:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P);
}
case medium:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P);
}
case low:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA);
}
default:
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW);
}

throw new IllegalArgumentException(
"No capture session available for current capture session.");
}
}

private void configureResolution(ResolutionPreset resolutionPreset, int cameraId)
throws IndexOutOfBoundsException {
if (!checkIsSupported()) {
return;
}
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);

if (Build.VERSION.SDK_INT >= 31) {
recordingProfile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
List<EncoderProfiles.VideoProfile> videoProfiles = recordingProfile.getVideoProfiles();

EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);
captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
} else {
@SuppressWarnings("deprecation")
CamcorderProfile camcorderProfile =
getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset);
recordingProfileLegacy = camcorderProfile;
captureSize =
new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight);
}

previewSize = computeBestPreviewSize(cameraId, resolutionPreset);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,55 @@
package io.flutter.plugins.camera.media;

import android.media.CamcorderProfile;
import android.media.EncoderProfiles;
import android.media.MediaRecorder;
import android.os.Build;
import androidx.annotation.NonNull;
import java.io.IOException;

public class MediaRecorderBuilder {
@SuppressWarnings("deprecation")
static class MediaRecorderFactory {
MediaRecorder makeMediaRecorder() {
return new MediaRecorder();
}
}

private final String outputFilePath;
private final CamcorderProfile recordingProfile;
private final CamcorderProfile camcorderProfile;
private final EncoderProfiles encoderProfiles;
private final MediaRecorderFactory recorderFactory;

private boolean enableAudio;
private int mediaOrientation;

public MediaRecorderBuilder(
@NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) {
this(recordingProfile, outputFilePath, new MediaRecorderFactory());
@NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath) {
this(camcorderProfile, outputFilePath, new MediaRecorderFactory());
}

public MediaRecorderBuilder(
@NonNull EncoderProfiles encoderProfiles, @NonNull String outputFilePath) {
this(encoderProfiles, outputFilePath, new MediaRecorderFactory());
}

MediaRecorderBuilder(
@NonNull CamcorderProfile recordingProfile,
@NonNull CamcorderProfile camcorderProfile,
@NonNull String outputFilePath,
MediaRecorderFactory helper) {
this.outputFilePath = outputFilePath;
this.recordingProfile = recordingProfile;
this.camcorderProfile = camcorderProfile;
this.encoderProfiles = null;
this.recorderFactory = helper;
}

MediaRecorderBuilder(
@NonNull EncoderProfiles encoderProfiles,
@NonNull String outputFilePath,
MediaRecorderFactory helper) {
this.outputFilePath = outputFilePath;
this.encoderProfiles = encoderProfiles;
this.camcorderProfile = null;
this.recorderFactory = helper;
}

Expand All @@ -47,23 +67,43 @@ public MediaRecorderBuilder setMediaOrientation(int orientation) {
return this;
}

public MediaRecorder build() throws IOException {
public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException {
MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder();

// There's a fixed order that mediaRecorder expects. Only change these functions accordingly.
// You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder.
if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(recordingProfile.fileFormat);
if (enableAudio) {
mediaRecorder.setAudioEncoder(recordingProfile.audioCodec);
mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate);
mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate);

if (Build.VERSION.SDK_INT >= 31) {
EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0);
EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0);

mediaRecorder.setOutputFormat(encoderProfiles.getRecommendedFileFormat());
if (enableAudio) {
mediaRecorder.setAudioEncoder(audioProfile.getCodec());
mediaRecorder.setAudioEncodingBitRate(audioProfile.getBitrate());
mediaRecorder.setAudioSamplingRate(audioProfile.getSampleRate());
}
mediaRecorder.setVideoEncoder(videoProfile.getCodec());
mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate());
mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate());
mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight());
mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight());
} else {
mediaRecorder.setOutputFormat(camcorderProfile.fileFormat);
if (enableAudio) {
mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec);
mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate);
mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate);
}
mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec);
mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate);
mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate);
mediaRecorder.setVideoSize(
camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
}
mediaRecorder.setVideoEncoder(recordingProfile.videoCodec);
mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate);
mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate);
mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);

mediaRecorder.setOutputFile(outputFilePath);
mediaRecorder.setOrientationHint(this.mediaOrientation);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
Expand Down Expand Up @@ -249,20 +248,6 @@ public void getMinZoomLevel() {
assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0);
}

@Test
public void getRecordingProfile() {
ResolutionFeature mockResolutionFeature =
mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null);
CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class);

when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile);

CamcorderProfile actualRecordingProfile = camera.getRecordingProfile();

verify(mockResolutionFeature, times(1)).getRecordingProfile();
assertEquals(mockCamcorderProfile, actualRecordingProfile);
}

@Test
public void setExposureMode_shouldUpdateExposureLockFeature() {
ExposureLockFeature mockExposureLockFeature =
Expand Down
Loading

0 comments on commit 29f46b4

Please sign in to comment.