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

Commit 29f46b4

Browse files
authored
[camera] Fix CamcorderProfile Usages (#4423)
1 parent 7588dd9 commit 29f46b4

File tree

13 files changed

+659
-67
lines changed

13 files changed

+659
-67
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 0.9.4+3
22

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

58
## 0.9.4+2

packages/camera/camera/android/build.gradle

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ buildscript {
99
}
1010

1111
dependencies {
12-
classpath 'com.android.tools.build:gradle:3.5.0'
12+
classpath 'com.android.tools.build:gradle:7.0.2'
1313
}
1414
}
1515

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

2929
android {
30-
compileSdkVersion 29
30+
compileSdkVersion 31
3131

3232
defaultConfig {
33+
targetSdkVersion 31
3334
minSdkVersion 21
3435
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
3536
}
@@ -60,7 +61,7 @@ android {
6061
dependencies {
6162
compileOnly 'androidx.annotation:annotation:1.1.0'
6263
testImplementation 'junit:junit:4.12'
63-
testImplementation 'org.mockito:mockito-inline:3.12.4'
64+
testImplementation 'org.mockito:mockito-inline:4.0.0'
6465
testImplementation 'androidx.test:core:1.3.0'
65-
testImplementation 'org.robolectric:robolectric:4.3'
66+
testImplementation 'org.robolectric:robolectric:4.5'
6667
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import android.hardware.camera2.params.OutputConfiguration;
2121
import android.hardware.camera2.params.SessionConfiguration;
2222
import android.media.CamcorderProfile;
23+
import android.media.EncoderProfiles;
2324
import android.media.Image;
2425
import android.media.ImageReader;
2526
import android.media.MediaRecorder;
@@ -199,8 +200,16 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException {
199200
((SensorOrientationFeature) cameraFeatures.getSensorOrientation())
200201
.getLockedCaptureOrientation();
201202

203+
MediaRecorderBuilder mediaRecorderBuilder;
204+
205+
if (Build.VERSION.SDK_INT >= 31) {
206+
mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath);
207+
} else {
208+
mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath);
209+
}
210+
202211
mediaRecorder =
203-
new MediaRecorderBuilder(getRecordingProfile(), outputFilePath)
212+
mediaRecorderBuilder
204213
.setEnableAudio(enableAudio)
205214
.setMediaOrientation(
206215
lockedOrientation == null
@@ -918,8 +927,12 @@ public float getMinZoomLevel() {
918927
return cameraFeatures.getZoomLevel().getMinimumZoomLevel();
919928
}
920929

921-
/** Shortcut to get current recording profile. */
922-
CamcorderProfile getRecordingProfile() {
930+
/** Shortcut to get current recording profile. Legacy method provides support for SDK < 31. */
931+
CamcorderProfile getRecordingProfileLegacy() {
932+
return cameraFeatures.getResolution().getRecordingProfileLegacy();
933+
}
934+
935+
EncoderProfiles getRecordingProfile() {
923936
return cameraFeatures.getResolution().getRecordingProfile();
924937
}
925938

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

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

7+
import android.annotation.TargetApi;
78
import android.hardware.camera2.CaptureRequest;
89
import android.media.CamcorderProfile;
10+
import android.media.EncoderProfiles;
11+
import android.os.Build;
912
import android.util.Size;
1013
import androidx.annotation.VisibleForTesting;
1114
import io.flutter.plugins.camera.CameraProperties;
1215
import io.flutter.plugins.camera.features.CameraFeature;
16+
import java.util.List;
1317

1418
/**
1519
* Controls the resolutions configuration on the {@link android.hardware.camera2} API.
@@ -21,7 +25,8 @@
2125
public class ResolutionFeature extends CameraFeature<ResolutionPreset> {
2226
private Size captureSize;
2327
private Size previewSize;
24-
private CamcorderProfile recordingProfile;
28+
private CamcorderProfile recordingProfileLegacy;
29+
private EncoderProfiles recordingProfile;
2530
private ResolutionPreset currentSetting;
2631
private int cameraId;
2732

@@ -51,7 +56,11 @@ public ResolutionFeature(
5156
*
5257
* @return Resolution information to configure the {@link android.hardware.camera2} API.
5358
*/
54-
public CamcorderProfile getRecordingProfile() {
59+
public CamcorderProfile getRecordingProfileLegacy() {
60+
return this.recordingProfileLegacy;
61+
}
62+
63+
public EncoderProfiles getRecordingProfile() {
5564
return this.recordingProfile;
5665
}
5766

@@ -100,19 +109,29 @@ public void updateBuilder(CaptureRequest.Builder requestBuilder) {
100109
}
101110

102111
@VisibleForTesting
103-
static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) {
112+
static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset)
113+
throws IndexOutOfBoundsException {
104114
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
105115
preset = ResolutionPreset.high;
106116
}
117+
if (Build.VERSION.SDK_INT >= 31) {
118+
EncoderProfiles profile =
119+
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
120+
List<EncoderProfiles.VideoProfile> videoProfiles = profile.getVideoProfiles();
121+
EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);
107122

108-
CamcorderProfile profile =
109-
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset);
110-
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
123+
return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
124+
} else {
125+
@SuppressWarnings("deprecation")
126+
CamcorderProfile profile =
127+
getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset);
128+
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
129+
}
111130
}
112131

113132
/**
114133
* Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link
115-
* ResolutionPreset}.
134+
* ResolutionPreset}. Supports SDK < 31.
116135
*
117136
* @param cameraId Camera identifier which indicates the device's camera for which to select a
118137
* {@link android.media.CamcorderProfile}.
@@ -121,7 +140,7 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) {
121140
* @return The best possible {@link android.media.CamcorderProfile} that matches the supplied
122141
* {@link ResolutionPreset}.
123142
*/
124-
public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
143+
public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPresetLegacy(
125144
int cameraId, ResolutionPreset preset) {
126145
if (cameraId < 0) {
127146
throw new AssertionError(
@@ -164,13 +183,74 @@ public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPres
164183
}
165184
}
166185

167-
private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) {
186+
@TargetApi(Build.VERSION_CODES.S)
187+
public static EncoderProfiles getBestAvailableCamcorderProfileForResolutionPreset(
188+
int cameraId, ResolutionPreset preset) {
189+
if (cameraId < 0) {
190+
throw new AssertionError(
191+
"getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers.");
192+
}
193+
194+
String cameraIdString = Integer.toString(cameraId);
195+
196+
switch (preset) {
197+
// All of these cases deliberately fall through to get the best available profile.
198+
case max:
199+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
200+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_HIGH);
201+
}
202+
case ultraHigh:
203+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
204+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_2160P);
205+
}
206+
case veryHigh:
207+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
208+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_1080P);
209+
}
210+
case high:
211+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
212+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_720P);
213+
}
214+
case medium:
215+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
216+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_480P);
217+
}
218+
case low:
219+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
220+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_QVGA);
221+
}
222+
default:
223+
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
224+
return CamcorderProfile.getAll(cameraIdString, CamcorderProfile.QUALITY_LOW);
225+
}
226+
227+
throw new IllegalArgumentException(
228+
"No capture session available for current capture session.");
229+
}
230+
}
231+
232+
private void configureResolution(ResolutionPreset resolutionPreset, int cameraId)
233+
throws IndexOutOfBoundsException {
168234
if (!checkIsSupported()) {
169235
return;
170236
}
171-
recordingProfile =
172-
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
173-
captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
237+
238+
if (Build.VERSION.SDK_INT >= 31) {
239+
recordingProfile =
240+
getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset);
241+
List<EncoderProfiles.VideoProfile> videoProfiles = recordingProfile.getVideoProfiles();
242+
243+
EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0);
244+
captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight());
245+
} else {
246+
@SuppressWarnings("deprecation")
247+
CamcorderProfile camcorderProfile =
248+
getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset);
249+
recordingProfileLegacy = camcorderProfile;
250+
captureSize =
251+
new Size(recordingProfileLegacy.videoFrameWidth, recordingProfileLegacy.videoFrameHeight);
252+
}
253+
174254
previewSize = computeBestPreviewSize(cameraId, resolutionPreset);
175255
}
176256
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,55 @@
55
package io.flutter.plugins.camera.media;
66

77
import android.media.CamcorderProfile;
8+
import android.media.EncoderProfiles;
89
import android.media.MediaRecorder;
10+
import android.os.Build;
911
import androidx.annotation.NonNull;
1012
import java.io.IOException;
1113

1214
public class MediaRecorderBuilder {
15+
@SuppressWarnings("deprecation")
1316
static class MediaRecorderFactory {
1417
MediaRecorder makeMediaRecorder() {
1518
return new MediaRecorder();
1619
}
1720
}
1821

1922
private final String outputFilePath;
20-
private final CamcorderProfile recordingProfile;
23+
private final CamcorderProfile camcorderProfile;
24+
private final EncoderProfiles encoderProfiles;
2125
private final MediaRecorderFactory recorderFactory;
2226

2327
private boolean enableAudio;
2428
private int mediaOrientation;
2529

2630
public MediaRecorderBuilder(
27-
@NonNull CamcorderProfile recordingProfile, @NonNull String outputFilePath) {
28-
this(recordingProfile, outputFilePath, new MediaRecorderFactory());
31+
@NonNull CamcorderProfile camcorderProfile, @NonNull String outputFilePath) {
32+
this(camcorderProfile, outputFilePath, new MediaRecorderFactory());
33+
}
34+
35+
public MediaRecorderBuilder(
36+
@NonNull EncoderProfiles encoderProfiles, @NonNull String outputFilePath) {
37+
this(encoderProfiles, outputFilePath, new MediaRecorderFactory());
2938
}
3039

3140
MediaRecorderBuilder(
32-
@NonNull CamcorderProfile recordingProfile,
41+
@NonNull CamcorderProfile camcorderProfile,
3342
@NonNull String outputFilePath,
3443
MediaRecorderFactory helper) {
3544
this.outputFilePath = outputFilePath;
36-
this.recordingProfile = recordingProfile;
45+
this.camcorderProfile = camcorderProfile;
46+
this.encoderProfiles = null;
47+
this.recorderFactory = helper;
48+
}
49+
50+
MediaRecorderBuilder(
51+
@NonNull EncoderProfiles encoderProfiles,
52+
@NonNull String outputFilePath,
53+
MediaRecorderFactory helper) {
54+
this.outputFilePath = outputFilePath;
55+
this.encoderProfiles = encoderProfiles;
56+
this.camcorderProfile = null;
3757
this.recorderFactory = helper;
3858
}
3959

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

50-
public MediaRecorder build() throws IOException {
70+
public MediaRecorder build() throws IOException, NullPointerException, IndexOutOfBoundsException {
5171
MediaRecorder mediaRecorder = recorderFactory.makeMediaRecorder();
5272

5373
// There's a fixed order that mediaRecorder expects. Only change these functions accordingly.
5474
// You can find the specifics here: https://developer.android.com/reference/android/media/MediaRecorder.
5575
if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
5676
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
57-
mediaRecorder.setOutputFormat(recordingProfile.fileFormat);
58-
if (enableAudio) {
59-
mediaRecorder.setAudioEncoder(recordingProfile.audioCodec);
60-
mediaRecorder.setAudioEncodingBitRate(recordingProfile.audioBitRate);
61-
mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate);
77+
78+
if (Build.VERSION.SDK_INT >= 31) {
79+
EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0);
80+
EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0);
81+
82+
mediaRecorder.setOutputFormat(encoderProfiles.getRecommendedFileFormat());
83+
if (enableAudio) {
84+
mediaRecorder.setAudioEncoder(audioProfile.getCodec());
85+
mediaRecorder.setAudioEncodingBitRate(audioProfile.getBitrate());
86+
mediaRecorder.setAudioSamplingRate(audioProfile.getSampleRate());
87+
}
88+
mediaRecorder.setVideoEncoder(videoProfile.getCodec());
89+
mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate());
90+
mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate());
91+
mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight());
92+
mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight());
93+
} else {
94+
mediaRecorder.setOutputFormat(camcorderProfile.fileFormat);
95+
if (enableAudio) {
96+
mediaRecorder.setAudioEncoder(camcorderProfile.audioCodec);
97+
mediaRecorder.setAudioEncodingBitRate(camcorderProfile.audioBitRate);
98+
mediaRecorder.setAudioSamplingRate(camcorderProfile.audioSampleRate);
99+
}
100+
mediaRecorder.setVideoEncoder(camcorderProfile.videoCodec);
101+
mediaRecorder.setVideoEncodingBitRate(camcorderProfile.videoBitRate);
102+
mediaRecorder.setVideoFrameRate(camcorderProfile.videoFrameRate);
103+
mediaRecorder.setVideoSize(
104+
camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
62105
}
63-
mediaRecorder.setVideoEncoder(recordingProfile.videoCodec);
64-
mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate);
65-
mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate);
66-
mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight);
106+
67107
mediaRecorder.setOutputFile(outputFilePath);
68108
mediaRecorder.setOrientationHint(this.mediaOrientation);
69109

packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import android.hardware.camera2.CameraCaptureSession;
2323
import android.hardware.camera2.CameraMetadata;
2424
import android.hardware.camera2.CaptureRequest;
25-
import android.media.CamcorderProfile;
2625
import android.media.MediaRecorder;
2726
import android.os.Build;
2827
import android.os.Handler;
@@ -249,20 +248,6 @@ public void getMinZoomLevel() {
249248
assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0);
250249
}
251250

252-
@Test
253-
public void getRecordingProfile() {
254-
ResolutionFeature mockResolutionFeature =
255-
mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null);
256-
CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class);
257-
258-
when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile);
259-
260-
CamcorderProfile actualRecordingProfile = camera.getRecordingProfile();
261-
262-
verify(mockResolutionFeature, times(1)).getRecordingProfile();
263-
assertEquals(mockCamcorderProfile, actualRecordingProfile);
264-
}
265-
266251
@Test
267252
public void setExposureMode_shouldUpdateExposureLockFeature() {
268253
ExposureLockFeature mockExposureLockFeature =

0 commit comments

Comments
 (0)