Skip to content

Commit 3dc59f9

Browse files
authored
[camera_android] Add NV21 as an image stream format flutter#3277 (flutter#3639)
This contains the changes for camera_android from flutter/packages#3277
1 parent 5bc70c7 commit 3dc59f9

File tree

10 files changed

+696
-46
lines changed

10 files changed

+696
-46
lines changed

packages/camera/camera_android/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.10.7
2+
3+
* Adds support for NV21 as a new streaming format in Android which includes correct handling of
4+
image padding when present.
5+
16
## 0.10.6+2
27

38
* Fixes compatibility with AGP versions older than 4.2.

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

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import android.hardware.camera2.params.SessionConfiguration;
2222
import android.media.CamcorderProfile;
2323
import android.media.EncoderProfiles;
24-
import android.media.Image;
2524
import android.media.ImageReader;
2625
import android.media.MediaRecorder;
2726
import android.os.Build;
@@ -58,19 +57,18 @@
5857
import io.flutter.plugins.camera.features.resolution.ResolutionPreset;
5958
import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager;
6059
import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature;
60+
import io.flutter.plugins.camera.media.ImageStreamReader;
6161
import io.flutter.plugins.camera.media.MediaRecorderBuilder;
6262
import io.flutter.plugins.camera.types.CameraCaptureProperties;
6363
import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper;
6464
import io.flutter.view.TextureRegistry.SurfaceTextureEntry;
6565
import java.io.File;
6666
import java.io.IOException;
67-
import java.nio.ByteBuffer;
6867
import java.util.ArrayList;
6968
import java.util.Arrays;
7069
import java.util.HashMap;
7170
import java.util.List;
7271
import java.util.Locale;
73-
import java.util.Map;
7472
import java.util.concurrent.Executors;
7573

7674
@FunctionalInterface
@@ -90,6 +88,7 @@ class Camera
9088
supportedImageFormats = new HashMap<>();
9189
supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888);
9290
supportedImageFormats.put("jpeg", ImageFormat.JPEG);
91+
supportedImageFormats.put("nv21", ImageFormat.NV21);
9392
}
9493

9594
/**
@@ -131,7 +130,7 @@ class Camera
131130
CameraDeviceWrapper cameraDevice;
132131
CameraCaptureSession captureSession;
133132
private ImageReader pictureImageReader;
134-
ImageReader imageStreamReader;
133+
ImageStreamReader imageStreamReader;
135134
/** {@link CaptureRequest.Builder} for the camera preview */
136135
CaptureRequest.Builder previewRequestBuilder;
137136

@@ -306,7 +305,7 @@ public void open(String imageFormatGroup) throws CameraAccessException {
306305
imageFormat = ImageFormat.YUV_420_888;
307306
}
308307
imageStreamReader =
309-
ImageReader.newInstance(
308+
new ImageStreamReader(
310309
resolutionFeature.getPreviewSize().getWidth(),
311310
resolutionFeature.getPreviewSize().getHeight(),
312311
imageFormat,
@@ -536,7 +535,7 @@ private void startCapture(boolean record, boolean stream) throws CameraAccessExc
536535
surfaces.add(mediaRecorder.getSurface());
537536
successCallback = () -> mediaRecorder.start();
538537
}
539-
if (stream) {
538+
if (stream && imageStreamReader != null) {
540539
surfaces.add(imageStreamReader.getSurface());
541540
}
542541

@@ -1191,49 +1190,21 @@ public void onListen(Object o, EventChannel.EventSink imageStreamSink) {
11911190

11921191
@Override
11931192
public void onCancel(Object o) {
1194-
imageStreamReader.setOnImageAvailableListener(null, backgroundHandler);
1193+
if (imageStreamReader == null) {
1194+
return;
1195+
}
1196+
1197+
imageStreamReader.removeListener(backgroundHandler);
11951198
}
11961199
});
11971200
}
11981201

11991202
void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) {
1200-
imageStreamReader.setOnImageAvailableListener(
1201-
reader -> {
1202-
Image img = reader.acquireNextImage();
1203-
// Use acquireNextImage since image reader is only for one image.
1204-
if (img == null) return;
1205-
1206-
List<Map<String, Object>> planes = new ArrayList<>();
1207-
for (Image.Plane plane : img.getPlanes()) {
1208-
ByteBuffer buffer = plane.getBuffer();
1209-
1210-
byte[] bytes = new byte[buffer.remaining()];
1211-
buffer.get(bytes, 0, bytes.length);
1212-
1213-
Map<String, Object> planeBuffer = new HashMap<>();
1214-
planeBuffer.put("bytesPerRow", plane.getRowStride());
1215-
planeBuffer.put("bytesPerPixel", plane.getPixelStride());
1216-
planeBuffer.put("bytes", bytes);
1217-
1218-
planes.add(planeBuffer);
1219-
}
1203+
if (imageStreamReader == null) {
1204+
return;
1205+
}
12201206

1221-
Map<String, Object> imageBuffer = new HashMap<>();
1222-
imageBuffer.put("width", img.getWidth());
1223-
imageBuffer.put("height", img.getHeight());
1224-
imageBuffer.put("format", img.getFormat());
1225-
imageBuffer.put("planes", planes);
1226-
imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture());
1227-
imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime());
1228-
Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity();
1229-
imageBuffer.put(
1230-
"sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity);
1231-
1232-
final Handler handler = new Handler(Looper.getMainLooper());
1233-
handler.post(() -> imageStreamSink.success(imageBuffer));
1234-
img.close();
1235-
},
1236-
backgroundHandler);
1207+
imageStreamReader.subscribeListener(this.captureProps, imageStreamSink, backgroundHandler);
12371208
}
12381209

12391210
void closeCaptureSession() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
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.camera.media;
6+
7+
import android.graphics.ImageFormat;
8+
import android.media.Image;
9+
import android.media.ImageReader;
10+
import android.os.Handler;
11+
import android.os.Looper;
12+
import android.view.Surface;
13+
import androidx.annotation.NonNull;
14+
import androidx.annotation.VisibleForTesting;
15+
import io.flutter.plugin.common.EventChannel;
16+
import io.flutter.plugins.camera.types.CameraCaptureProperties;
17+
import java.nio.ByteBuffer;
18+
import java.util.ArrayList;
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
// Wraps an ImageReader to allow for testing of the image handler.
24+
public class ImageStreamReader {
25+
26+
/**
27+
* The image format we are going to send back to dart. Usually it's the same as streamImageFormat
28+
* but in the case of NV21 we will actually request YUV frames but convert it to NV21 before
29+
* sending to dart.
30+
*/
31+
private final int dartImageFormat;
32+
33+
private final ImageReader imageReader;
34+
private final ImageStreamReaderUtils imageStreamReaderUtils;
35+
36+
/**
37+
* Creates a new instance of the {@link ImageStreamReader}.
38+
*
39+
* @param imageReader is the image reader that will receive frames
40+
* @param imageStreamReaderUtils is an instance of {@link ImageStreamReaderUtils}
41+
*/
42+
@VisibleForTesting
43+
public ImageStreamReader(
44+
@NonNull ImageReader imageReader,
45+
int dartImageFormat,
46+
@NonNull ImageStreamReaderUtils imageStreamReaderUtils) {
47+
this.imageReader = imageReader;
48+
this.dartImageFormat = dartImageFormat;
49+
this.imageStreamReaderUtils = imageStreamReaderUtils;
50+
}
51+
52+
/**
53+
* Creates a new instance of the {@link ImageStreamReader}.
54+
*
55+
* @param width is the image width
56+
* @param height is the image height
57+
* @param imageFormat is the {@link ImageFormat} that should be returned to dart.
58+
* @param maxImages is how many images can be acquired at one time, usually 1.
59+
*/
60+
public ImageStreamReader(int width, int height, int imageFormat, int maxImages) {
61+
this.dartImageFormat = imageFormat;
62+
this.imageReader =
63+
ImageReader.newInstance(width, height, computeStreamImageFormat(imageFormat), maxImages);
64+
this.imageStreamReaderUtils = new ImageStreamReaderUtils();
65+
}
66+
67+
/**
68+
* Returns the image format to stream based on a requested input format. Usually it's the same
69+
* except when dart is requesting NV21. In that case we stream YUV420 and process it into NV21
70+
* before sending the frames over.
71+
*
72+
* @param dartImageFormat is the image format dart is requesting.
73+
* @return the image format that should be streamed from the camera.
74+
*/
75+
@VisibleForTesting
76+
public static int computeStreamImageFormat(int dartImageFormat) {
77+
if (dartImageFormat == ImageFormat.NV21) {
78+
return ImageFormat.YUV_420_888;
79+
} else {
80+
return dartImageFormat;
81+
}
82+
}
83+
84+
/**
85+
* Processes a new frame (image) from the image reader and send the frame to Dart.
86+
*
87+
* @param image is the image which needs processed as an {@link Image}
88+
* @param captureProps is the capture props from the camera class as {@link
89+
* CameraCaptureProperties}
90+
* @param imageStreamSink is the image stream sink from dart as a dart {@link
91+
* EventChannel.EventSink}
92+
*/
93+
@VisibleForTesting
94+
public void onImageAvailable(
95+
@NonNull Image image,
96+
@NonNull CameraCaptureProperties captureProps,
97+
@NonNull EventChannel.EventSink imageStreamSink) {
98+
try {
99+
Map<String, Object> imageBuffer = new HashMap<>();
100+
101+
// Get plane data ready
102+
if (dartImageFormat == ImageFormat.NV21) {
103+
imageBuffer.put("planes", parsePlanesForNv21(image));
104+
} else {
105+
imageBuffer.put("planes", parsePlanesForYuvOrJpeg(image));
106+
}
107+
108+
imageBuffer.put("width", image.getWidth());
109+
imageBuffer.put("height", image.getHeight());
110+
imageBuffer.put("format", dartImageFormat);
111+
imageBuffer.put("lensAperture", captureProps.getLastLensAperture());
112+
imageBuffer.put("sensorExposureTime", captureProps.getLastSensorExposureTime());
113+
Integer sensorSensitivity = captureProps.getLastSensorSensitivity();
114+
imageBuffer.put(
115+
"sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity);
116+
117+
final Handler handler = new Handler(Looper.getMainLooper());
118+
handler.post(() -> imageStreamSink.success(imageBuffer));
119+
image.close();
120+
121+
} catch (IllegalStateException e) {
122+
// Handle "buffer is inaccessible" errors that can happen on some devices from ImageStreamReaderUtils.yuv420ThreePlanesToNV21()
123+
final Handler handler = new Handler(Looper.getMainLooper());
124+
handler.post(
125+
() ->
126+
imageStreamSink.error(
127+
"IllegalStateException",
128+
"Caught IllegalStateException: " + e.getMessage(),
129+
null));
130+
image.close();
131+
}
132+
}
133+
134+
/**
135+
* Given an input image, will return a list of maps suitable to send back to dart where each map
136+
* describes the image plane.
137+
*
138+
* <p>For Yuv / Jpeg, we do no further processing on the frame so we simply send it as-is.
139+
*
140+
* @param image - the image to process.
141+
* @return parsed map describing the image planes to be sent to dart.
142+
*/
143+
@NonNull
144+
public List<Map<String, Object>> parsePlanesForYuvOrJpeg(@NonNull Image image) {
145+
List<Map<String, Object>> planes = new ArrayList<>();
146+
147+
// For YUV420 and JPEG, just send the data as-is for each plane.
148+
for (Image.Plane plane : image.getPlanes()) {
149+
ByteBuffer buffer = plane.getBuffer();
150+
151+
byte[] bytes = new byte[buffer.remaining()];
152+
buffer.get(bytes, 0, bytes.length);
153+
154+
Map<String, Object> planeBuffer = new HashMap<>();
155+
planeBuffer.put("bytesPerRow", plane.getRowStride());
156+
planeBuffer.put("bytesPerPixel", plane.getPixelStride());
157+
planeBuffer.put("bytes", bytes);
158+
159+
planes.add(planeBuffer);
160+
}
161+
return planes;
162+
}
163+
164+
/**
165+
* Given an input image, will return a single-plane NV21 image. Assumes YUV420 as an input type.
166+
*
167+
* @param image - the image to process.
168+
* @return parsed map describing the image planes to be sent to dart.
169+
*/
170+
@NonNull
171+
public List<Map<String, Object>> parsePlanesForNv21(@NonNull Image image) {
172+
List<Map<String, Object>> planes = new ArrayList<>();
173+
174+
// We will convert the YUV data to NV21 which is a single-plane image
175+
ByteBuffer bytes =
176+
imageStreamReaderUtils.yuv420ThreePlanesToNV21(
177+
image.getPlanes(), image.getWidth(), image.getHeight());
178+
179+
Map<String, Object> planeBuffer = new HashMap<>();
180+
planeBuffer.put("bytesPerRow", image.getWidth());
181+
planeBuffer.put("bytesPerPixel", 1);
182+
planeBuffer.put("bytes", bytes.array());
183+
planes.add(planeBuffer);
184+
return planes;
185+
}
186+
187+
/** Returns the image reader surface. */
188+
@NonNull
189+
public Surface getSurface() {
190+
return imageReader.getSurface();
191+
}
192+
193+
/**
194+
* Subscribes the image stream reader to handle incoming images using onImageAvailable().
195+
*
196+
* @param captureProps is the capture props from the camera class as {@link
197+
* CameraCaptureProperties}
198+
* @param imageStreamSink is the image stream sink from dart as {@link EventChannel.EventSink}
199+
* @param handler is generally the background handler of the camera as {@link Handler}
200+
*/
201+
public void subscribeListener(
202+
@NonNull CameraCaptureProperties captureProps,
203+
@NonNull EventChannel.EventSink imageStreamSink,
204+
@NonNull Handler handler) {
205+
imageReader.setOnImageAvailableListener(
206+
reader -> {
207+
Image image = reader.acquireNextImage();
208+
if (image == null) return;
209+
210+
onImageAvailable(image, captureProps, imageStreamSink);
211+
},
212+
handler);
213+
}
214+
215+
/**
216+
* Removes the listener from the image reader.
217+
*
218+
* @param handler is generally the background handler of the camera
219+
*/
220+
public void removeListener(@NonNull Handler handler) {
221+
imageReader.setOnImageAvailableListener(null, handler);
222+
}
223+
224+
/** Closes the image reader. */
225+
public void close() {
226+
imageReader.close();
227+
}
228+
}

0 commit comments

Comments
 (0)