Skip to content

Commit 6b77867

Browse files
Merge branch 'main' into main
2 parents d7ffa9e + 9c85e5e commit 6b77867

File tree

174 files changed

+3105
-4108
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

174 files changed

+3105
-4108
lines changed

.ci/flutter_master.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
adffe244f3f112e35736d08d85ab4cdbe8e13aa4
1+
70cdc0c933d6b84817d216891953b6eb56e22007

.ci/flutter_stable.version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
fcf2c11572af6f390246c056bc905eca609533a0
1+
d7b523b356d15fb81e7d340bbe52b47f93937323

.gemini/config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Minimize verbosity.
2+
have_fun: false
3+
code_review:
4+
# For now, use the default of MEDIUM for testing. Based on desired verbosity,
5+
# we can change this to LOW or HIGH in the future.
6+
comment_severity_threshold: MEDIUM
7+
pull_request_opened:
8+
# Explicitly set help to false in case the default changes in the future, as
9+
# having a help message on every PR would be spammy.
10+
help: false
11+
# These tend to be verbose, and since we expect PR authors to clearly
12+
# describe their PRs this would be at best duplicative.
13+
summary: false
14+
ignore_patterns:
15+
- .ci/flutter_master.version
16+
- .ci/flutter_stable.version

.gemini/styleguide.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Flutter Packages Style Guide
2+
3+
## Introduction
4+
5+
This style guide outlines the coding conventions for contributions to the
6+
flutter/packages repository.
7+
8+
## Style Guides
9+
10+
Code should follow the relevant style guides, and use the correct
11+
auto-formatter, for each language, as described in
12+
[the repository contributing guide's Style section](https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style).
13+
14+
## Best Practices
15+
16+
- Code should follow the guidance and principles described in
17+
[the flutter/packages contribution guide](https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md).
18+
- Code should be tested. Changes to plugin packages, which include code written
19+
in C, C++, Java, Kotlin, Objective-C, or Swift, should have appropriate tests
20+
as described in [the plugin test guidance](https://github.com/flutter/flutter/blob/master/docs/ecosystem/testing/Plugin-Tests.md).
21+
- PR descriptions should include the Pre-Review Checklist from
22+
[the PR template](https://github.com/flutter/packages/blob/main/.github/PULL_REQUEST_TEMPLATE.md),
23+
with all of the steps completed.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
If you need help, consider asking for advice on the #hackers-new channel on [Discord].
2020

21+
**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.
22+
2123
[^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.
2224

2325
<!-- Links -->

README.md

Lines changed: 44 additions & 44 deletions
Large diffs are not rendered by default.

customer_testing.sh

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ pushd packages/rfw
2929

3030
# Update the subpackages so that the analysis doesn't get confused.
3131
pushd example/remote; flutter packages get; popd
32-
pushd example/wasm; flutter packages get; popd
3332
pushd test_coverage; dart pub get; popd
3433

3534
flutter analyze --no-fatal-infos

packages/camera/camera_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.10.10+4
2+
3+
* Fix flutter#166533 - prevent startImageStream OOM error when main thread paused.
4+
15
## 0.10.10+3
26

37
* Waits for the creation of the capture session when initializing the camera to avoid thread race conditions.

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

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
import android.media.ImageReader;
1010
import android.os.Handler;
1111
import android.os.Looper;
12+
import android.util.Log;
1213
import android.view.Surface;
1314
import androidx.annotation.NonNull;
15+
import androidx.annotation.Nullable;
1416
import androidx.annotation.VisibleForTesting;
1517
import io.flutter.plugin.common.EventChannel;
1618
import io.flutter.plugins.camera.types.CameraCaptureProperties;
19+
import java.lang.ref.WeakReference;
1720
import java.nio.ByteBuffer;
1821
import java.util.ArrayList;
1922
import java.util.HashMap;
@@ -22,6 +25,7 @@
2225

2326
// Wraps an ImageReader to allow for testing of the image handler.
2427
public class ImageStreamReader {
28+
private static final String TAG = "ImageStreamReader";
2529

2630
/**
2731
* The image format we are going to send back to dart. Usually it's the same as streamImageFormat
@@ -33,6 +37,16 @@ public class ImageStreamReader {
3337
private final ImageReader imageReader;
3438
private final ImageStreamReaderUtils imageStreamReaderUtils;
3539

40+
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
41+
@Nullable
42+
public Handler handler;
43+
44+
/**
45+
* This hard reference is required so frames don't get randomly dropped before reaching the main
46+
* looper.
47+
*/
48+
private Map<String, Object> latestImageBufferHardReference = null;
49+
3650
/**
3751
* Creates a new instance of the {@link ImageStreamReader}.
3852
*
@@ -95,40 +109,69 @@ public void onImageAvailable(
95109
@NonNull Image image,
96110
@NonNull CameraCaptureProperties captureProps,
97111
@NonNull EventChannel.EventSink imageStreamSink) {
98-
try {
99-
Map<String, Object> imageBuffer = new HashMap<>();
112+
Map<String, Object> imageBuffer = new HashMap<>();
100113

114+
imageBuffer.put("width", image.getWidth());
115+
imageBuffer.put("height", image.getHeight());
116+
try {
101117
// Get plane data ready
102118
if (dartImageFormat == ImageFormat.NV21) {
103119
imageBuffer.put("planes", parsePlanesForNv21(image));
104120
} else {
105121
imageBuffer.put("planes", parsePlanesForYuvOrJpeg(image));
106122
}
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-
121123
} 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+
// Handle "buffer is inaccessible" errors that can happen on some devices from
125+
// ImageStreamReaderUtils.yuv420ThreePlanesToNV21()
126+
final Handler handler =
127+
this.handler != null ? this.handler : new Handler(Looper.getMainLooper());
124128
handler.post(
125129
() ->
126130
imageStreamSink.error(
127131
"IllegalStateException",
128132
"Caught IllegalStateException: " + e.getMessage(),
129133
null));
134+
} finally {
130135
image.close();
131136
}
137+
138+
imageBuffer.put("format", dartImageFormat);
139+
imageBuffer.put("lensAperture", captureProps.getLastLensAperture());
140+
imageBuffer.put("sensorExposureTime", captureProps.getLastSensorExposureTime());
141+
Integer sensorSensitivity = captureProps.getLastSensorSensitivity();
142+
imageBuffer.put(
143+
"sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity);
144+
145+
final Handler handler =
146+
this.handler != null ? this.handler : new Handler(Looper.getMainLooper());
147+
148+
// Keep a hard reference to the latest frame, so it isn't dropped before it reaches the main
149+
// looper
150+
latestImageBufferHardReference = imageBuffer;
151+
152+
boolean postResult =
153+
handler.post(
154+
new Runnable() {
155+
@VisibleForTesting public WeakReference<Map<String, Object>> weakImageBuffer;
156+
157+
public Runnable withImageBuffer(Map<String, Object> imageBuffer) {
158+
weakImageBuffer = new WeakReference<>(imageBuffer);
159+
return this;
160+
}
161+
162+
@Override
163+
public void run() {
164+
final Map<String, Object> imageBuffer = weakImageBuffer.get();
165+
if (imageBuffer == null) {
166+
// The memory was freed by the runtime, most likely due to a memory build-up
167+
// while the main thread was lagging. Frames are silently dropped in this
168+
// case.
169+
Log.d(TAG, "Image buffer was dropped by garbage collector.");
170+
return;
171+
}
172+
imageStreamSink.success(imageBuffer);
173+
}
174+
}.withImageBuffer(imageBuffer));
132175
}
133176

134177
/**

packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/ImageStreamReaderTest.java

Lines changed: 83 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,22 @@
99
import static org.mockito.ArgumentMatchers.anyInt;
1010
import static org.mockito.Mockito.mock;
1111
import static org.mockito.Mockito.never;
12+
import static org.mockito.Mockito.times;
1213
import static org.mockito.Mockito.verify;
1314
import static org.mockito.Mockito.when;
1415

1516
import android.graphics.ImageFormat;
1617
import android.media.Image;
1718
import android.media.ImageReader;
19+
import android.os.Handler;
1820
import io.flutter.plugin.common.EventChannel;
1921
import io.flutter.plugins.camera.types.CameraCaptureProperties;
22+
import java.lang.ref.WeakReference;
23+
import java.lang.reflect.Field;
2024
import java.nio.ByteBuffer;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.Map;
2128
import org.junit.Test;
2229
import org.junit.runner.RunWith;
2330
import org.robolectric.RobolectricTestRunner;
@@ -61,48 +68,18 @@ public void onImageAvailable_parsesPlanesForNv21() {
6168
when(mockImageStreamReaderUtils.yuv420ThreePlanesToNV21(any(), anyInt(), anyInt()))
6269
.thenReturn(mockBytes);
6370

64-
// The image format as streamed from the camera
65-
int imageFormat = ImageFormat.YUV_420_888;
66-
67-
// Mock YUV image
68-
Image mockImage = mock(Image.class);
69-
when(mockImage.getWidth()).thenReturn(1280);
70-
when(mockImage.getHeight()).thenReturn(720);
71-
when(mockImage.getFormat()).thenReturn(imageFormat);
72-
73-
// Mock planes. YUV images have 3 planes (Y, U, V).
74-
Image.Plane planeY = mock(Image.Plane.class);
75-
Image.Plane planeU = mock(Image.Plane.class);
76-
Image.Plane planeV = mock(Image.Plane.class);
77-
78-
// Y plane is width*height
79-
// Row stride is generally == width but when there is padding it will
80-
// be larger. The numbers in this example are from a Vivo V2135 on 'high'
81-
// setting (1280x720).
82-
when(planeY.getBuffer()).thenReturn(ByteBuffer.allocate(1105664));
83-
when(planeY.getRowStride()).thenReturn(1536);
84-
when(planeY.getPixelStride()).thenReturn(1);
85-
86-
// U and V planes are always the same sizes/values.
87-
// https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
88-
when(planeU.getBuffer()).thenReturn(ByteBuffer.allocate(552703));
89-
when(planeV.getBuffer()).thenReturn(ByteBuffer.allocate(552703));
90-
when(planeU.getRowStride()).thenReturn(1536);
91-
when(planeV.getRowStride()).thenReturn(1536);
92-
when(planeU.getPixelStride()).thenReturn(2);
93-
when(planeV.getPixelStride()).thenReturn(2);
94-
95-
// Add planes to image
96-
Image.Plane[] planes = {planeY, planeU, planeV};
97-
when(mockImage.getPlanes()).thenReturn(planes);
71+
// Note: the code for getImage() was previously inlined, with uSize set to one less than
72+
// getImage() calculates (see function implementation)
73+
Image mockImage = ImageStreamReaderTestUtils.getImage(1280, 720, 256, ImageFormat.YUV_420_888);
9874

9975
CameraCaptureProperties mockCaptureProps = mock(CameraCaptureProperties.class);
10076
EventChannel.EventSink mockEventSink = mock(EventChannel.EventSink.class);
10177
imageStreamReader.onImageAvailable(mockImage, mockCaptureProps, mockEventSink);
10278

10379
// Make sure we processed the frame with parsePlanesForNv21
10480
verify(mockImageStreamReaderUtils)
105-
.yuv420ThreePlanesToNV21(planes, mockImage.getWidth(), mockImage.getHeight());
81+
.yuv420ThreePlanesToNV21(
82+
mockImage.getPlanes(), mockImage.getWidth(), mockImage.getHeight());
10683
}
10784

10885
/** If we are requesting YUV420, then we should send the 3-plane image as it is. */
@@ -120,40 +97,9 @@ public void onImageAvailable_parsesPlanesForYuv420() {
12097
when(mockImageStreamReaderUtils.yuv420ThreePlanesToNV21(any(), anyInt(), anyInt()))
12198
.thenReturn(mockBytes);
12299

123-
// The image format as streamed from the camera
124-
int imageFormat = ImageFormat.YUV_420_888;
125-
126-
// Mock YUV image
127-
Image mockImage = mock(Image.class);
128-
when(mockImage.getWidth()).thenReturn(1280);
129-
when(mockImage.getHeight()).thenReturn(720);
130-
when(mockImage.getFormat()).thenReturn(imageFormat);
131-
132-
// Mock planes. YUV images have 3 planes (Y, U, V).
133-
Image.Plane planeY = mock(Image.Plane.class);
134-
Image.Plane planeU = mock(Image.Plane.class);
135-
Image.Plane planeV = mock(Image.Plane.class);
136-
137-
// Y plane is width*height
138-
// Row stride is generally == width but when there is padding it will
139-
// be larger. The numbers in this example are from a Vivo V2135 on 'high'
140-
// setting (1280x720).
141-
when(planeY.getBuffer()).thenReturn(ByteBuffer.allocate(1105664));
142-
when(planeY.getRowStride()).thenReturn(1536);
143-
when(planeY.getPixelStride()).thenReturn(1);
144-
145-
// U and V planes are always the same sizes/values.
146-
// https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888
147-
when(planeU.getBuffer()).thenReturn(ByteBuffer.allocate(552703));
148-
when(planeV.getBuffer()).thenReturn(ByteBuffer.allocate(552703));
149-
when(planeU.getRowStride()).thenReturn(1536);
150-
when(planeV.getRowStride()).thenReturn(1536);
151-
when(planeU.getPixelStride()).thenReturn(2);
152-
when(planeV.getPixelStride()).thenReturn(2);
153-
154-
// Add planes to image
155-
Image.Plane[] planes = {planeY, planeU, planeV};
156-
when(mockImage.getPlanes()).thenReturn(planes);
100+
// Note: the code for getImage() was previously inlined, with uSize set to one less than
101+
// getImage() calculates (see function implementation)
102+
Image mockImage = ImageStreamReaderTestUtils.getImage(1280, 720, 256, ImageFormat.YUV_420_888);
157103

158104
CameraCaptureProperties mockCaptureProps = mock(CameraCaptureProperties.class);
159105
EventChannel.EventSink mockEventSink = mock(EventChannel.EventSink.class);
@@ -162,4 +108,72 @@ public void onImageAvailable_parsesPlanesForYuv420() {
162108
// Make sure we processed the frame with parsePlanesForYuvOrJpeg
163109
verify(mockImageStreamReaderUtils, never()).yuv420ThreePlanesToNV21(any(), anyInt(), anyInt());
164110
}
111+
112+
@Test
113+
public void onImageAvailable_dropFramesWhenHandlerHalted() {
114+
int dartImageFormat = ImageFormat.YUV_420_888;
115+
116+
ImageReader mockImageReader = mock(ImageReader.class);
117+
ImageStreamReaderUtils mockImageStreamReaderUtils = mock(ImageStreamReaderUtils.class);
118+
ImageStreamReader imageStreamReader =
119+
new ImageStreamReader(mockImageReader, dartImageFormat, mockImageStreamReaderUtils);
120+
121+
for (boolean invalidateWeakReference : new boolean[] {true, false}) {
122+
final List<Runnable> runnables = new ArrayList<Runnable>();
123+
124+
Handler mockHandler = mock(Handler.class);
125+
imageStreamReader.handler = mockHandler;
126+
127+
// initially, handler will simulate a hanging main looper, that only queues inputs
128+
when(mockHandler.post(any(Runnable.class)))
129+
.thenAnswer(
130+
inputs -> {
131+
Runnable r = inputs.getArgument(0, Runnable.class);
132+
runnables.add(r);
133+
return true;
134+
});
135+
136+
CameraCaptureProperties mockCaptureProps = mock(CameraCaptureProperties.class);
137+
EventChannel.EventSink mockEventSink = mock(EventChannel.EventSink.class);
138+
139+
Image mockImage =
140+
ImageStreamReaderTestUtils.getImage(1280, 720, 256, ImageFormat.YUV_420_888);
141+
imageStreamReader.onImageAvailable(mockImage, mockCaptureProps, mockEventSink);
142+
143+
// make sure the image was closed, even when skipping frames
144+
verify(mockImage, times(1)).close();
145+
146+
// check that we collected all runnables in this method
147+
assertEquals(runnables.size(), 1);
148+
149+
// verify post() was not called more times than it should have
150+
verify(mockHandler, times(1)).post(any(Runnable.class));
151+
152+
// make sure callback was not yet invoked
153+
verify(mockEventSink, never()).success(any(Map.class));
154+
155+
// simulate frame processing
156+
for (Runnable r : runnables) {
157+
if (invalidateWeakReference) {
158+
// Replace the captured WeakReference with one pointing to null.
159+
Field[] fields = r.getClass().getDeclaredFields();
160+
for (Field field : fields) {
161+
if (field.getType().equals(WeakReference.class)) {
162+
// Remove the `final` modifier
163+
try {
164+
field.set(r, new WeakReference<Map<String, Object>>(null));
165+
} catch (IllegalAccessException e) {
166+
throw new RuntimeException("Failed to inject null WeakReference", e);
167+
}
168+
}
169+
}
170+
}
171+
172+
r.run();
173+
}
174+
175+
// make sure all callbacks were invoked so far
176+
verify(mockEventSink, invalidateWeakReference ? never() : times(1)).success(any(Map.class));
177+
}
178+
}
165179
}

0 commit comments

Comments
 (0)