Skip to content

Commit f419532

Browse files
authored
[camera_android_camerax] Fix NV21 Format (#10022)
When NV21 image format is requested for streaming images, this PR ensures that: 1. The NV21-compatible (but actually YUV_420_888) three planes per image are converted to a single NV21 plane (Y + interleaved VU planes). 2. The single `CameraImagePlane` created has overridden raw and camera image format NV21.* This should make this package compatible with [google_ml_kit_flutter](https://github.com/flutter-ml/google_ml_kit_flutter/tree/master) 🤞 I tested this change with the Barcode scanner example. Fixes flutter/flutter#174923. *The conversion will fail if the image is actually not NV21 compatible for some reason, so this should never be a false positive. _Note: Uses code inspired by [googlesamples/mlkit](https://github.com/googlesamples/mlkit/blob/da17257a78b9beedb57b7a9795b911296ae970a0/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java)._ ## 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 a3af613 commit f419532

File tree

13 files changed

+718
-13
lines changed

13 files changed

+718
-13
lines changed

packages/camera/camera_android_camerax/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.6.23
2+
3+
* Converts NV21-compatible streamed images to NV21 when requested. In doing so,
4+
this plugin should now be compatible with [google_ml_kit_flutter](https://github.com/flutter-ml/google_ml_kit_flutter/tree/master).
5+
16
## 0.6.22
27

38
* Implements `setDescriptionWhileRecording`.

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

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,12 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary
597597
*/
598598
abstract fun getPigeonApiImageProxy(): PigeonApiImageProxy
599599

600+
/**
601+
* An implementation of [PigeonApiImageProxyUtils] used to add a new Dart instance of
602+
* `ImageProxyUtils` to the Dart `InstanceManager`.
603+
*/
604+
abstract fun getPigeonApiImageProxyUtils(): PigeonApiImageProxyUtils
605+
600606
/**
601607
* An implementation of [PigeonApiPlaneProxy] used to add a new Dart instance of `PlaneProxy` to
602608
* the Dart `InstanceManager`.
@@ -738,6 +744,7 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary
738744
PigeonApiAnalyzer.setUpMessageHandlers(binaryMessenger, getPigeonApiAnalyzer())
739745
PigeonApiLiveData.setUpMessageHandlers(binaryMessenger, getPigeonApiLiveData())
740746
PigeonApiImageProxy.setUpMessageHandlers(binaryMessenger, getPigeonApiImageProxy())
747+
PigeonApiImageProxyUtils.setUpMessageHandlers(binaryMessenger, getPigeonApiImageProxyUtils())
741748
PigeonApiQualitySelector.setUpMessageHandlers(binaryMessenger, getPigeonApiQualitySelector())
742749
PigeonApiFallbackStrategy.setUpMessageHandlers(binaryMessenger, getPigeonApiFallbackStrategy())
743750
PigeonApiCameraControl.setUpMessageHandlers(binaryMessenger, getPigeonApiCameraControl())
@@ -785,6 +792,7 @@ abstract class CameraXLibraryPigeonProxyApiRegistrar(val binaryMessenger: Binary
785792
PigeonApiAnalyzer.setUpMessageHandlers(binaryMessenger, null)
786793
PigeonApiLiveData.setUpMessageHandlers(binaryMessenger, null)
787794
PigeonApiImageProxy.setUpMessageHandlers(binaryMessenger, null)
795+
PigeonApiImageProxyUtils.setUpMessageHandlers(binaryMessenger, null)
788796
PigeonApiQualitySelector.setUpMessageHandlers(binaryMessenger, null)
789797
PigeonApiFallbackStrategy.setUpMessageHandlers(binaryMessenger, null)
790798
PigeonApiCameraControl.setUpMessageHandlers(binaryMessenger, null)
@@ -916,6 +924,8 @@ private class CameraXLibraryPigeonProxyApiBaseCodec(
916924
registrar.getPigeonApiLiveData().pigeon_newInstance(value) {}
917925
} else if (value is androidx.camera.core.ImageProxy) {
918926
registrar.getPigeonApiImageProxy().pigeon_newInstance(value) {}
927+
} else if (value is ImageProxyUtils) {
928+
registrar.getPigeonApiImageProxyUtils().pigeon_newInstance(value) {}
919929
} else if (value is androidx.camera.core.ImageProxy.PlaneProxy) {
920930
registrar.getPigeonApiPlaneProxy().pigeon_newInstance(value) {}
921931
} else if (value is androidx.camera.video.QualitySelector) {
@@ -5372,6 +5382,83 @@ abstract class PigeonApiImageProxy(
53725382
}
53735383
}
53745384
}
5385+
/** Utils for working with [ImageProxy]s. */
5386+
@Suppress("UNCHECKED_CAST")
5387+
abstract class PigeonApiImageProxyUtils(
5388+
open val pigeonRegistrar: CameraXLibraryPigeonProxyApiRegistrar
5389+
) {
5390+
/**
5391+
* Returns a single Byte Buffer that is representative of the [planes] that are NV21 compatible.
5392+
*/
5393+
abstract fun getNv21Buffer(
5394+
imageWidth: Long,
5395+
imageHeight: Long,
5396+
planes: List<androidx.camera.core.ImageProxy.PlaneProxy>
5397+
): ByteArray
5398+
5399+
companion object {
5400+
@Suppress("LocalVariableName")
5401+
fun setUpMessageHandlers(binaryMessenger: BinaryMessenger, api: PigeonApiImageProxyUtils?) {
5402+
val codec = api?.pigeonRegistrar?.codec ?: CameraXLibraryPigeonCodec()
5403+
run {
5404+
val channel =
5405+
BasicMessageChannel<Any?>(
5406+
binaryMessenger,
5407+
"dev.flutter.pigeon.camera_android_camerax.ImageProxyUtils.getNv21Buffer",
5408+
codec)
5409+
if (api != null) {
5410+
channel.setMessageHandler { message, reply ->
5411+
val args = message as List<Any?>
5412+
val imageWidthArg = args[0] as Long
5413+
val imageHeightArg = args[1] as Long
5414+
val planesArg = args[2] as List<androidx.camera.core.ImageProxy.PlaneProxy>
5415+
val wrapped: List<Any?> =
5416+
try {
5417+
listOf(api.getNv21Buffer(imageWidthArg, imageHeightArg, planesArg))
5418+
} catch (exception: Throwable) {
5419+
CameraXLibraryPigeonUtils.wrapError(exception)
5420+
}
5421+
reply.reply(wrapped)
5422+
}
5423+
} else {
5424+
channel.setMessageHandler(null)
5425+
}
5426+
}
5427+
}
5428+
}
5429+
5430+
@Suppress("LocalVariableName", "FunctionName")
5431+
/** Creates a Dart instance of ImageProxyUtils and attaches it to [pigeon_instanceArg]. */
5432+
fun pigeon_newInstance(pigeon_instanceArg: ImageProxyUtils, callback: (Result<Unit>) -> Unit) {
5433+
if (pigeonRegistrar.ignoreCallsToDart) {
5434+
callback(
5435+
Result.failure(
5436+
CameraXError("ignore-calls-error", "Calls to Dart are being ignored.", "")))
5437+
} else if (pigeonRegistrar.instanceManager.containsInstance(pigeon_instanceArg)) {
5438+
callback(Result.success(Unit))
5439+
} else {
5440+
val pigeon_identifierArg =
5441+
pigeonRegistrar.instanceManager.addHostCreatedInstance(pigeon_instanceArg)
5442+
val binaryMessenger = pigeonRegistrar.binaryMessenger
5443+
val codec = pigeonRegistrar.codec
5444+
val channelName =
5445+
"dev.flutter.pigeon.camera_android_camerax.ImageProxyUtils.pigeon_newInstance"
5446+
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
5447+
channel.send(listOf(pigeon_identifierArg)) {
5448+
if (it is List<*>) {
5449+
if (it.size > 1) {
5450+
callback(
5451+
Result.failure(CameraXError(it[0] as String, it[1] as String, it[2] as String?)))
5452+
} else {
5453+
callback(Result.success(Unit))
5454+
}
5455+
} else {
5456+
callback(Result.failure(CameraXLibraryPigeonUtils.createConnectionError(channelName)))
5457+
}
5458+
}
5459+
}
5460+
}
5461+
}
53755462
/**
53765463
* A plane proxy which has an analogous interface as `android.media.Image.Plane`.
53775464
*
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.camera.core.ImageProxy.PlaneProxy;
9+
import java.nio.ByteBuffer;
10+
import java.util.List;
11+
12+
/* Utilities for working with {@code ImageProxy}s. */
13+
public class ImageProxyUtils {
14+
15+
/**
16+
* Converts list of {@link PlaneProxy}s in YUV_420_888 format (with VU planes in NV21 layout) to a
17+
* single NV21 {@code ByteBuffer}.
18+
*/
19+
@NonNull
20+
public static ByteBuffer planesToNV21(@NonNull List<PlaneProxy> planes, int width, int height) {
21+
if (!areUVPlanesNV21(planes, width, height)) {
22+
throw new IllegalArgumentException(
23+
"Provided UV planes are not in NV21 layout and thus cannot be converted.");
24+
}
25+
26+
int imageSize = width * height;
27+
int nv21Size = imageSize + 2 * (imageSize / 4);
28+
byte[] nv21Bytes = new byte[nv21Size];
29+
30+
// Copy Y plane.
31+
ByteBuffer yBuffer = planes.get(0).getBuffer();
32+
yBuffer.rewind();
33+
yBuffer.get(nv21Bytes, 0, imageSize);
34+
35+
// Copy interleaved VU plane (NV21 layout).
36+
ByteBuffer vBuffer = planes.get(2).getBuffer();
37+
ByteBuffer uBuffer = planes.get(1).getBuffer();
38+
39+
vBuffer.rewind();
40+
uBuffer.rewind();
41+
vBuffer.get(nv21Bytes, imageSize, 1);
42+
uBuffer.get(nv21Bytes, imageSize + 1, 2 * imageSize / 4 - 1);
43+
44+
return ByteBuffer.wrap(nv21Bytes);
45+
}
46+
47+
public static boolean areUVPlanesNV21(@NonNull List<PlaneProxy> planes, int width, int height) {
48+
int imageSize = width * height;
49+
50+
ByteBuffer uBuffer = planes.get(1).getBuffer();
51+
ByteBuffer vBuffer = planes.get(2).getBuffer();
52+
53+
// Backup buffer properties.
54+
int vBufferPosition = vBuffer.position();
55+
int uBufferLimit = uBuffer.limit();
56+
57+
// Advance the V buffer by 1 byte, since the U buffer will not contain the first V value.
58+
vBuffer.position(vBufferPosition + 1);
59+
// Chop off the last byte of the U buffer, since the V buffer will not contain the last U value.
60+
uBuffer.limit(uBufferLimit - 1);
61+
62+
// Check that the buffers are equal and have the expected number of elements.
63+
boolean areNV21 =
64+
(vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0);
65+
66+
// Restore buffers to their initial state.
67+
vBuffer.position(vBufferPosition);
68+
uBuffer.limit(uBufferLimit);
69+
70+
return areNV21;
71+
}
72+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import androidx.annotation.NonNull;
8+
import androidx.camera.core.ImageProxy.PlaneProxy;
9+
import java.nio.ByteBuffer;
10+
import java.util.List;
11+
12+
/**
13+
* ProxyApi implementation for {@link ImageProxyUtils}. This class may handle instantiating native
14+
* object instances that are attached to a Dart instance or handle method calls on the associated
15+
* native class or an instance of that class.
16+
*/
17+
public class ImageProxyUtilsProxyApi extends PigeonApiImageProxyUtils {
18+
ImageProxyUtilsProxyApi(@NonNull ProxyApiRegistrar pigeonRegistrar) {
19+
super(pigeonRegistrar);
20+
}
21+
22+
// List<? extends PlaneProxy> can be considered the same as List<PlaneProxy>.
23+
@SuppressWarnings("unchecked")
24+
@NonNull
25+
@Override
26+
public byte[] getNv21Buffer(
27+
long imageWidth, long imageHeight, @NonNull List<? extends PlaneProxy> planes) {
28+
final ByteBuffer nv21Buffer =
29+
ImageProxyUtils.planesToNV21(
30+
(List<PlaneProxy>) planes, (int) imageWidth, (int) imageHeight);
31+
32+
byte[] bytes = new byte[nv21Buffer.remaining()];
33+
nv21Buffer.get(bytes, 0, bytes.length);
34+
35+
return bytes;
36+
}
37+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,4 +425,10 @@ public PigeonApiMeteringPointFactory getPigeonApiMeteringPointFactory() {
425425
public CameraPermissionsErrorProxyApi getPigeonApiCameraPermissionsError() {
426426
return new CameraPermissionsErrorProxyApi(this);
427427
}
428+
429+
@NonNull
430+
@Override
431+
public PigeonApiImageProxyUtils getPigeonApiImageProxyUtils() {
432+
return new ImageProxyUtilsProxyApi(this);
433+
}
428434
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.camerax;
6+
7+
import static org.junit.Assert.assertArrayEquals;
8+
import static org.mockito.Mockito.mockStatic;
9+
10+
import androidx.camera.core.ImageProxy.PlaneProxy;
11+
import java.nio.ByteBuffer;
12+
import java.util.Arrays;
13+
import java.util.List;
14+
import org.junit.Test;
15+
import org.mockito.MockedStatic;
16+
import org.mockito.Mockito;
17+
18+
public class ImageProxyUtilsApiTest {
19+
20+
@Test
21+
public void getNv21Buffer_returnsExpectedBytes() {
22+
final PigeonApiImageProxyUtils api = new TestProxyApiRegistrar().getPigeonApiImageProxyUtils();
23+
24+
List<PlaneProxy> planes =
25+
Arrays.asList(
26+
Mockito.mock(PlaneProxy.class),
27+
Mockito.mock(PlaneProxy.class),
28+
Mockito.mock(PlaneProxy.class));
29+
long width = 4;
30+
long height = 2;
31+
byte[] expectedBytes = new byte[] {1, 2, 3, 4, 5};
32+
ByteBuffer mockBuffer = ByteBuffer.wrap(expectedBytes);
33+
34+
try (MockedStatic<ImageProxyUtils> mockedStatic = mockStatic(ImageProxyUtils.class)) {
35+
mockedStatic
36+
.when(
37+
() ->
38+
ImageProxyUtils.planesToNV21(
39+
Mockito.anyList(), Mockito.anyInt(), Mockito.anyInt()))
40+
.thenReturn(mockBuffer);
41+
42+
byte[] result = api.getNv21Buffer(width, height, planes);
43+
44+
assertArrayEquals(expectedBytes, result);
45+
mockedStatic.verify(() -> ImageProxyUtils.planesToNV21(planes, (int) width, (int) height));
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)