-
Notifications
You must be signed in to change notification settings - Fork 9.8k
[camera] Fixed a crash when streaming on iOS #4520
Conversation
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat. If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the contribution!
}); | ||
|
||
await Future.delayed(Duration(seconds: 5)); | ||
expect(_frame > 30, true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like this is testing round-trip speed on the device it's running on, rather than anything about the code itself. Where is the value of 30
coming from? How do you know that this will always pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surely 30 is not important, so I removed it.
I wanted to test if I could keep it running for a certain amount of time without crashing.
I'm not sure if 5 seconds is a reasonable number in this case.
Actually, I don't know how to create such environment-dependent tests.
@@ -240,4 +240,34 @@ void main() { | |||
}, | |||
skip: !Platform.isAndroid, | |||
); | |||
|
|||
testWidgets( | |||
'Image streaming persistence test on iOS', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this test fail if the rest of the PR were reverted? It doesn't seem that way, unless I'm missing something about the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a device dependent test, so if you test it on an old device, it will crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means that it wouldn't actually fail in our CI, which means there's nothing preventing this from breaking again.
A test for something like this should be creating the conditions to allow for testing that the behavior is what we want regardless of the device, so that it has predictable output. If the goal is to prevent more than N frames from being in flight without response, for instance, then a test should deliberately respond very slowly and assert that N isn't exceeded.
packages/camera/camera/CHANGELOG.md
Outdated
@@ -2,6 +2,7 @@ | |||
|
|||
* Fixes bug where calling a method after the camera was closed resulted in a Java `IllegalStateException` exception. | |||
* Fixes integration tests. | |||
* Fixes crash when streaming in iOS. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes cannot be added to versions that have already been published.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed it.
@@ -438,6 +438,13 @@ class CameraController extends ValueNotifier<CameraValue> { | |||
_imageStreamSubscription = | |||
cameraEventChannel.receiveBroadcastStream().listen( | |||
(dynamic imageData) { | |||
if (defaultTargetPlatform == TargetPlatform.iOS) { | |||
try { | |||
_channel.invokeMethod<void>('receivedImageStreamData'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this is asynchronous in both directions I would expect this could cause significant frame loss even on a reasonably powerful device. How much investigation have you done into specifically what the parameters of the crash are? Is it really necessary to have a maximum of a single frame in flight?
It seems likely that this needs a more nuanced approach (maybe a hard limit still but higher than one, maybe something that's tunable by plugin clients). It's hard to determine the right fix without more details about what the failure condition actually is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modified the plugin to allow you to set a limit on the maximum number of frames pending processing.
And I tested it on my devices to see how many frames can be processed in 5 seconds. The results are shown in the table below.
The value is the number of frames processed at max quality and is the average of three times.
The horizontal axis is the maximum number of pending frames (frameStack), where 0 means no limit.
With no limit, iPhone6 and iPadAir crashed.
In this case, if frameStack is set to about 4, frame loss can be reduced to some extent.
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
---|---|---|---|---|---|---|---|
iPhoneX | 143 | 76 | 108 | 118 | 133 | 138 | 136 |
iPhone6 | 13(crash) | 40 | 53 | 69 | 70 | 55 | 56 |
iPadAir-1st | 14(crash) | 44 | 62 | 53 | 63 | 52 | 45 |
I don't have that many devices, but according to flutter/flutter#44436, it seems to be crashing on iPhone 6, 7, 8, etc. These devices still have a large market share that cannot be ignored. Also, for image analysis processes such as OCR, low or medium quality is not always satisfactory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this testing. Given this table, I'm fine with starting from setting a fixed value—4 looks like a good place to start—with an explanation in the code of where that number came from, and then if it turns out that causes issues in some cases we can add an API to tune it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Updating review status for the comments above)
…streaming # Conflicts: # packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj # packages/camera/camera/ios/Classes/CameraPlugin.m
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's looking good
id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); | ||
[_camera startImageStreamWithMessenger:messenger imageStreamHandler:handlerMock]; | ||
|
||
[self waitForStart]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i highly recommend not to create this helper function since it's making the test a bit harder to read. Unit tests themselves are not unit tested, so it's better to make it as simple as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Fixed.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"isStreamingImages == YES"]; | ||
XCTNSPredicateExpectation *expectation = | ||
[[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:_camera]; | ||
XCTWaiterResult result = [XCTWaiter waitForExpectations:@[ expectation ] timeout:3]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm curious will it fail if we use 1 second? due to predicate parsing? if so please document it out. otherwise let's keep 1 second.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also i think you can do [self expectationForPredicate...]
and [self waitForExpectations:]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using XCTNSPredicateExpectation, it seems to time out with a 1 second wait.
I tried using KVO this time and it seems to work well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Just a few minor style issues to resolve.
@@ -60,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN | |||
- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; | |||
- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; | |||
- (void)applyFocusMode; | |||
- (void)receivedImageStreamData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a declaration comment, per style guide.
(I know almost nothing else in this header current has one, but existing style violations aren't a reason to perpetuate new style violations.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
But, my English isn't very good, so please point out any oddities.
@@ -5,6 +5,14 @@ | |||
#import "FLTCam.h" | |||
#import "FLTSavePhotoDelegate.h" | |||
|
|||
@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler> | |||
// The queue on which `eventSink` property should be accessed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: These are declaration comments, so should follow declaration comment style (which is ///
for this file).
@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler> | ||
// The queue on which `eventSink` property should be accessed | ||
@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; | ||
// `eventSink` property should be accessed on `captureSessionQueue`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blank line between declarations please.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few more minor nits on the comments. Thanks for adding them!
@@ -60,6 +60,10 @@ NS_ASSUME_NONNULL_BEGIN | |||
- (void)setExposureModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; | |||
- (void)setFocusModeWithResult:(FLTThreadSafeFlutterResult *)result mode:(NSString *)modeStr; | |||
- (void)applyFocusMode; | |||
|
|||
/// Sets that a streaming image has been received. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Sets" implies that there's a boolean toggle here, which isn't the case. And the last sentence of this comment isn't relevant to clients using the method.
I wold replace this whole declaration comment with something like:
/**
* Acknowledges the receipt of one image stream frame.
*
* This should be called each time a frame is received. Failing to call it may
* cause later frames to be dropped instead of streamed.
*/
(This file uses /** ... */ style. Ideally we'd be consistent within the whole project, but we should at least have file-level consistency.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I learned a lot. Thank you.
I ended up using your suggestion in its entirety.
@@ -6,11 +6,14 @@ | |||
#import "FLTSavePhotoDelegate.h" | |||
|
|||
@interface FLTImageStreamHandler : NSObject <FlutterStreamHandler> | |||
// The queue on which `eventSink` property should be accessed | |||
|
|||
/// The queue on which `eventSink` property should be accessed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: missing period.
@@ -135,6 +133,11 @@ - (instancetype)initWithCameraName:(NSString *)cameraName | |||
_videoFormat = kCVPixelFormatType_32BGRA; | |||
_inProgressSavePhotoDelegates = [NSMutableDictionary dictionary]; | |||
|
|||
// To prevent memory consumption, limit the number of frames pending processing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: change "prevent" to "limit".
@property(nonatomic, strong) dispatch_queue_t captureSessionQueue; | ||
|
||
/// `eventSink` property should be accessed on `captureSessionQueue`. | ||
/// The block itself should be invoked on the main queue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A declaration comment should start wit a description of what something is, before getting into how to use it. So:
/// The event sink to stream camera events to Dart.
///
/// The property should only be accessed on `captureSessionQueue`.
/// The block itself should be invoked on the main queue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
is it fixed in flutter 3.0 ? |
This was fixed in camera plugin version 0.9.4+18, not a version of Flutter. |
ok, I have still a crash with the last version on iOS (it works on android)
edit: seems more related to googleMLKit addon.. |
I'm getting crash on video recording on camera v0.9.7+1 and the latest one (camera v0.9.8+1) *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '*** -[AVAssetWriterInputPixelBufferAdaptor appendPixelBuffer:withPresentationTime:] A pixel buffer cannot be appended when readyForMoreMediaData is NO.'
|
The place to report new crashes is the issue tracker; this is a fix for an unrelated crash. |
Fixes flutter/flutter#44436
Streaming large images on older devices seems to cause the call stack to fill up as the processing cannot keep up.
Added a confirmation of reception for each frame, and skipped frames that could not be processed.
Pre-launch Checklist
dart format
.)[shared_preferences]
pubspec.yaml
with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.CHANGELOG.md
to add a description of the change, following repository CHANGELOG style.///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.