Skip to content

Commit c905585

Browse files
authored
[camera_android_camerax] Support NV21 (#9853)
Implements NV21 support for image streaming. Also bumps the CameraX version to `1.5.0-rc01`! To do this, I needed to (1) bump the minimum SDK version to 23 and (2) modify a couple of unrelated tests. Fixes flutter/flutter#145961. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 86fbeec commit c905585

File tree

17 files changed

+1047
-718
lines changed

17 files changed

+1047
-718
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.6.21
2+
3+
* Implements NV21 support for image streaming.
4+
15
## 0.6.20+3
26

37
* Bumps com.google.guava:guava from 33.4.0-android to 33.4.8-android.

packages/camera/camera_android_camerax/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ in the merged Android manifest of your app, then take the following steps to rem
7373
tools:node="remove" />
7474
```
7575

76-
### Allowing image streaming in the background
76+
### Notes on image streaming
77+
78+
#### Allowing image streaming in the background
7779

7880
As of Android 14, to allow for background image streaming, you will need to specify the foreground
7981
[`TYPE_CAMERA`][12] foreground service permission in your app's manifest. Specifically, in
@@ -86,6 +88,12 @@ As of Android 14, to allow for background image streaming, you will need to spec
8688
</manifest>
8789
```
8890

91+
#### Configuring NV21 image format
92+
93+
If you initialize a `CameraController` with `ImageFormatGroup.nv21`, then streamed images will
94+
still have the `ImageFormatGroup.yuv420` format, but their image data will be formatted in NV21.
95+
See https://developer.android.com/reference/kotlin/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21().
96+
8997
## Contributing
9098

9199
For more information on contributing to this plugin, see [`CONTRIBUTING.md`](CONTRIBUTING.md).

packages/camera/camera_android_camerax/android/build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ android {
4040
}
4141

4242
defaultConfig {
43-
// Many of the CameraX APIs require API 21.
44-
minSdkVersion 21
43+
// CameraX APIs require API 23 or later.
44+
minSdk = 23
4545
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
4646
}
4747

@@ -72,7 +72,7 @@ android {
7272

7373
dependencies {
7474
// CameraX core library using the camera2 implementation must use same version number.
75-
def camerax_version = "1.5.0-beta01"
75+
def camerax_version = "1.5.0-rc01"
7676
implementation "androidx.camera:camera-core:${camerax_version}"
7777
implementation "androidx.camera:camera-camera2:${camerax_version}"
7878
implementation "androidx.camera:camera-lifecycle:${camerax_version}"

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4-
// Autogenerated from Pigeon (v25.3.2), do not edit directly.
4+
// Autogenerated from Pigeon (v25.5.0), do not edit directly.
55
// See also: https://pub.dev/packages/pigeon
66
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
77

@@ -4761,7 +4761,8 @@ abstract class PigeonApiImageAnalysis(
47614761
) {
47624762
abstract fun pigeon_defaultConstructor(
47634763
resolutionSelector: androidx.camera.core.resolutionselector.ResolutionSelector?,
4764-
targetRotation: Long?
4764+
targetRotation: Long?,
4765+
outputImageFormat: Long?
47654766
): androidx.camera.core.ImageAnalysis
47664767

47674768
abstract fun resolutionSelector(
@@ -4800,10 +4801,12 @@ abstract class PigeonApiImageAnalysis(
48004801
val resolutionSelectorArg =
48014802
args[1] as androidx.camera.core.resolutionselector.ResolutionSelector?
48024803
val targetRotationArg = args[2] as Long?
4804+
val outputImageFormatArg = args[3] as Long?
48034805
val wrapped: List<Any?> =
48044806
try {
48054807
api.pigeonRegistrar.instanceManager.addDartCreatedInstance(
4806-
api.pigeon_defaultConstructor(resolutionSelectorArg, targetRotationArg),
4808+
api.pigeon_defaultConstructor(
4809+
resolutionSelectorArg, targetRotationArg, outputImageFormatArg),
48074810
pigeon_identifierArg)
48084811
listOf(null)
48094812
} catch (exception: Throwable) {

packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisProxyApi.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,21 @@ class ImageAnalysisProxyApi extends PigeonApiImageAnalysis {
2121
@NonNull
2222
@Override
2323
public ImageAnalysis pigeon_defaultConstructor(
24-
@Nullable ResolutionSelector resolutionSelector, @Nullable Long targetRotation) {
24+
@Nullable ResolutionSelector resolutionSelector,
25+
@Nullable Long targetRotation,
26+
@Nullable Long outputImageFormat) {
2527
final ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
2628
if (resolutionSelector != null) {
2729
builder.setResolutionSelector(resolutionSelector);
2830
}
2931
if (targetRotation != null) {
3032
builder.setTargetRotation(targetRotation.intValue());
3133
}
34+
35+
if (outputImageFormat != null) {
36+
builder.setOutputImageFormat(outputImageFormat.intValue());
37+
}
38+
3239
return builder.build();
3340
}
3441

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/Camera2CameraInfoTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void getCameraCharacteristic_returnsCorrespondingValueOfKeyWhenKeyNotReco
6969
assertEquals(value, api.getCameraCharacteristic(instance, key));
7070
}
7171

72-
@Config(minSdk = 21)
72+
@Config(minSdk = 23)
7373
@SuppressWarnings("unchecked")
7474
@Test
7575
public void getCameraCharacteristic_returnsExpectedCameraHardwareLevelWhenRequested() {

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ public void pigeon_defaultConstructor_createsExpectedImageAnalysisInstance() {
3232

3333
final ResolutionSelector mockResolutionSelector = new ResolutionSelector.Builder().build();
3434
final long targetResolution = Surface.ROTATION_0;
35+
final long outputImageFormat = ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21;
3536
final ImageAnalysis imageAnalysis =
36-
api.pigeon_defaultConstructor(mockResolutionSelector, targetResolution);
37+
api.pigeon_defaultConstructor(mockResolutionSelector, targetResolution, outputImageFormat);
3738

3839
assertEquals(imageAnalysis.getResolutionSelector(), mockResolutionSelector);
3940
assertEquals(imageAnalysis.getTargetRotation(), Surface.ROTATION_0);
41+
assertEquals(imageAnalysis.getOutputImageFormat(), ImageAnalysis.OUTPUT_IMAGE_FORMAT_NV21);
4042
}
4143

4244
@Test

packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PendingRecordingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public void start_callsStartOnInstance() {
9898
.when(() -> ContextCompat.getMainExecutor(any()))
9999
.thenAnswer((Answer<Executor>) invocation -> mock(Executor.class));
100100

101-
when(instance.start(any(), any())).thenReturn(value);
101+
when(instance.start(any(Executor.class), any())).thenReturn(value);
102102

103103
assertEquals(value, api.start(instance, listener));
104104
}

packages/camera/camera_android_camerax/example/lib/camera_image.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) {
8383
// android.graphics.ImageFormat.YUV_420_888
8484
case 35:
8585
return ImageFormatGroup.yuv420;
86+
// android.graphics.ImageFormat.NV21
87+
case 17:
88+
return ImageFormatGroup.nv21;
8689
// android.graphics.ImageFormat.JPEG
8790
case 256:
8891
return ImageFormatGroup.jpeg;

packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart

Lines changed: 89 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,30 @@ class AndroidCameraCameraX extends CameraPlatform {
174174
@visibleForTesting
175175
StreamController<CameraImageData>? cameraImageDataStreamController;
176176

177-
/// Constant representing the multi-plane Android YUV 420 image format.
177+
/// Constant representing the multi-plane Android YUV 420 image format used by ImageProxy.
178178
///
179179
/// See https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888.
180-
static const int imageFormatYuv420_888 = 35;
180+
static const int imageProxyFormatYuv420_888 = 35;
181181

182-
/// Constant representing the compressed JPEG image format.
182+
/// Constant representing the NV21 image format used by ImageProxy.
183+
///
184+
/// See https://developer.android.com/reference/android/graphics/ImageFormat#NV21.
185+
static const int imageProxyFormatNv21 = 17;
186+
187+
/// Constant representing the compressed JPEG image format used by ImageProxy.
183188
///
184189
/// See https://developer.android.com/reference/android/graphics/ImageFormat#JPEG.
185-
static const int imageFormatJpeg = 256;
190+
static const int imageProxyFormatJpeg = 256;
191+
192+
/// Constant representing the YUV 420 image format used for configuring ImageAnalysis.
193+
///
194+
/// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_YUV_420_888()
195+
static const int imageAnalysisOutputImageFormatYuv420_888 = 1;
196+
197+
/// Constant representing the NV21 image format used for configuring ImageAnalysis.
198+
///
199+
/// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis#OUTPUT_IMAGE_FORMAT_NV21().
200+
static const int imageAnalysisOutputImageFormatNv21 = 3;
186201

187202
/// Error code indicating a [ZoomState] was requested, but one has not been
188203
/// set for the camera in use.
@@ -269,6 +284,12 @@ class AndroidCameraCameraX extends CameraPlatform {
269284
/// A map to associate a [CameraInfo] with its camera name.
270285
final Map<String, CameraInfo> _savedCameras = <String, CameraInfo>{};
271286

287+
/// The preset resolution selector for the camera.
288+
ResolutionSelector? _presetResolutionSelector;
289+
290+
/// The ID of the surface texture that the camera preview is drawn to.
291+
late int _flutterSurfaceTextureId;
292+
272293
/// Returns list of all available cameras and their descriptions.
273294
@override
274295
Future<List<CameraDescription>> availableCameras() async {
@@ -380,8 +401,9 @@ class AndroidCameraCameraX extends CameraPlatform {
380401
);
381402
// Determine ResolutionSelector and QualitySelector based on
382403
// resolutionPreset for camera UseCases.
383-
final ResolutionSelector? presetResolutionSelector =
384-
_getResolutionSelectorFromPreset(mediaSettings?.resolutionPreset);
404+
_presetResolutionSelector = _getResolutionSelectorFromPreset(
405+
mediaSettings?.resolutionPreset,
406+
);
385407
final QualitySelector? presetQualitySelector =
386408
_getQualitySelectorFromPreset(mediaSettings?.resolutionPreset);
387409

@@ -391,55 +413,26 @@ class AndroidCameraCameraX extends CameraPlatform {
391413

392414
// Configure Preview instance.
393415
preview = proxy.newPreview(
394-
resolutionSelector: presetResolutionSelector,
416+
resolutionSelector: _presetResolutionSelector,
395417
/* use CameraX default target rotation */ targetRotation: null,
396418
);
397-
final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(
419+
_flutterSurfaceTextureId = await preview!.setSurfaceProvider(
398420
systemServicesManager,
399421
);
400422

401423
// Configure ImageCapture instance.
402424
imageCapture = proxy.newImageCapture(
403-
resolutionSelector: presetResolutionSelector,
425+
resolutionSelector: _presetResolutionSelector,
404426
/* use CameraX default target rotation */ targetRotation:
405427
await deviceOrientationManager.getDefaultDisplayRotation(),
406428
);
407429

408-
// Configure ImageAnalysis instance.
409-
// Defaults to YUV_420_888 image format.
410-
imageAnalysis = proxy.newImageAnalysis(
411-
resolutionSelector: presetResolutionSelector,
412-
/* use CameraX default target rotation */ targetRotation: null,
413-
);
414-
415430
// Configure VideoCapture and Recorder instances.
416431
recorder = proxy.newRecorder(qualitySelector: presetQualitySelector);
417432
videoCapture = proxy.withOutputVideoCapture(videoOutput: recorder!);
418433

419-
// Bind configured UseCases to ProcessCameraProvider instance & mark Preview
420-
// instance as bound but not paused. Video capture is bound at first use
421-
// instead of here.
422-
camera = await processCameraProvider!.bindToLifecycle(
423-
cameraSelector!,
424-
<UseCase>[preview!, imageCapture!, imageAnalysis!],
425-
);
426-
await _updateCameraInfoAndLiveCameraState(flutterSurfaceTextureId);
427-
previewInitiallyBound = true;
428-
_previewIsPaused = false;
429-
430434
// Retrieve info required for correcting the rotation of the camera preview
431435
// if necessary.
432-
433-
final Camera2CameraInfo camera2CameraInfo = proxy.fromCamera2CameraInfo(
434-
cameraInfo: cameraInfo!,
435-
);
436-
sensorOrientationDegrees =
437-
((await camera2CameraInfo.getCameraCharacteristic(
438-
proxy.sensorOrientationCameraCharacteristics(),
439-
))!
440-
as int)
441-
.toDouble();
442-
443436
sensorOrientationDegrees = cameraDescription.sensorOrientation.toDouble();
444437
_handlesCropAndRotation = await preview!
445438
.surfaceProducerHandlesCropAndRotation();
@@ -449,35 +442,59 @@ class AndroidCameraCameraX extends CameraPlatform {
449442
_initialDefaultDisplayRotation = await deviceOrientationManager
450443
.getDefaultDisplayRotation();
451444

452-
return flutterSurfaceTextureId;
445+
return _flutterSurfaceTextureId;
453446
}
454447

455448
/// Initializes the camera on the device.
456449
///
457-
/// Since initialization of a camera does not directly map as an operation to
458-
/// the CameraX library, this method just retrieves information about the
459-
/// camera and sends a [CameraInitializedEvent].
450+
/// Specifically, this method:
451+
/// * Configures the [ImageAnalysis] instance according to the specified
452+
/// [imageFormatGroup]
453+
/// * Binds the configured [Preview], [ImageCapture], and [ImageAnalysis]
454+
/// instances to the [ProcessCameraProvider] instance.
455+
/// * Retrieves information about the camera and sends a [CameraInitializedEvent].
460456
///
461457
/// [imageFormatGroup] is used to specify the image format used for image
462-
/// streaming, but CameraX currently only supports YUV_420_888 (supported by
463-
/// Flutter) and RGBA (not supported by Flutter). CameraX uses YUV_420_888
464-
/// by default, so [imageFormatGroup] is not used.
458+
/// streaming, but CameraX currently only supports YUV_420_888 (the CameraX default),
459+
/// NV21, and RGBA (not supported by Flutter).
465460
@override
466461
Future<void> initializeCamera(
467462
int cameraId, {
468463
ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown,
469464
}) async {
470-
// Configure CameraInitializedEvent to send as representation of a
471-
// configured camera:
472-
// Retrieve preview resolution.
465+
// If preview has not been created, then no camera has been created, which signals that
466+
// createCamera was not called before initializeCamera.
473467
if (preview == null) {
474-
// No camera has been created; createCamera must be called before initializeCamera.
475468
throw CameraException(
476469
'cameraNotFound',
477470
"Camera not found. Please call the 'create' method before calling 'initialize'",
478471
);
479472
}
473+
// Configure ImageAnalysis instance.
474+
// Defaults to YUV_420_888 image format.
475+
imageAnalysis = proxy.newImageAnalysis(
476+
resolutionSelector: _presetResolutionSelector,
477+
outputImageFormat: _imageAnalysisOutputFormatFromImageFormatGroup(
478+
imageFormatGroup,
479+
),
480+
/* use CameraX default target rotation */ targetRotation: null,
481+
);
482+
483+
// Bind configured UseCases to ProcessCameraProvider instance & mark Preview
484+
// instance as bound but not paused. Video capture is bound at first use
485+
// instead of here.
486+
camera = await processCameraProvider!.bindToLifecycle(
487+
cameraSelector!,
488+
<UseCase>[preview!, imageCapture!, imageAnalysis!],
489+
);
490+
await _updateCameraInfoAndLiveCameraState(_flutterSurfaceTextureId);
491+
previewInitiallyBound = true;
492+
_previewIsPaused = false;
493+
494+
// Configure CameraInitializedEvent to send as representation of a
495+
// configured camera:
480496

497+
// Retrieve preview resolution.
481498
final ResolutionInfo previewResolutionInfo = (await preview!
482499
.getResolutionInfo())!;
483500

@@ -1214,6 +1231,10 @@ class AndroidCameraCameraX extends CameraPlatform {
12141231
/// implementation using a broadcast [StreamController], which does not
12151232
/// support those operations.
12161233
///
1234+
/// If the camera was initialized with [ImageFormatGroup.nv21], then the
1235+
/// streamed images will still have format [ImageFormatGroup.yuv420], but
1236+
/// their image data will be formatted in NV21.
1237+
///
12171238
/// [cameraId] and [options] are not used.
12181239
@override
12191240
Stream<CameraImageData> onStreamedFrameAvailable(
@@ -1326,14 +1347,30 @@ class AndroidCameraCameraX extends CameraPlatform {
13261347
await imageAnalysis!.clearAnalyzer();
13271348
}
13281349

1329-
/// Converts between Android ImageFormat constants and [ImageFormatGroup]s.
1350+
/// Converts [ImageFormatGroup]s to Android ImageAnalysis output format constants.
1351+
///
1352+
/// See https://developer.android.com/reference/androidx/camera/core/ImageAnalysis.
1353+
int? _imageAnalysisOutputFormatFromImageFormatGroup(dynamic format) {
1354+
switch (format) {
1355+
case ImageFormatGroup.yuv420:
1356+
return imageAnalysisOutputImageFormatYuv420_888;
1357+
case ImageFormatGroup.nv21:
1358+
return imageAnalysisOutputImageFormatNv21;
1359+
}
1360+
1361+
return null;
1362+
}
1363+
1364+
/// Converts from Android ImageFormat constants to [ImageFormatGroup]s.
13301365
///
13311366
/// See https://developer.android.com/reference/android/graphics/ImageFormat.
13321367
ImageFormatGroup _imageFormatGroupFromPlatformData(dynamic data) {
13331368
switch (data) {
1334-
case imageFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888
1369+
case imageProxyFormatYuv420_888: // android.graphics.ImageFormat.YUV_420_888
13351370
return ImageFormatGroup.yuv420;
1336-
case imageFormatJpeg: // android.graphics.ImageFormat.JPEG
1371+
case imageProxyFormatNv21: // android.graphics.ImageFormat.NV21
1372+
return ImageFormatGroup.nv21;
1373+
case imageProxyFormatJpeg: // android.graphics.ImageFormat.JPEG
13371374
return ImageFormatGroup.jpeg;
13381375
}
13391376

0 commit comments

Comments
 (0)