Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[camera] Fixed a crash when streaming on iOS #4520

Merged
merged 37 commits into from
Mar 29, 2022
Merged

[camera] Fixed a crash when streaming on iOS #4520

merged 37 commits into from
Mar 29, 2022

Conversation

zuvola
Copy link
Contributor

@zuvola zuvola commented Nov 18, 2021

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

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the relevant style guides and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/plugins repo does use dart format.)
  • I signed the CLA.
  • The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences]
  • I listed at least one issue that this PR fixes in the description above.
  • I updated pubspec.yaml with an appropriate new version according to the pub versioning philosophy, or this PR is exempt from version changes.
  • I updated CHANGELOG.md to add a description of the change, following repository CHANGELOG style.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • All existing and new tests are passing.

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

@flutter-dashboard
Copy link

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.

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a 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);
Copy link
Contributor

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?

Copy link
Contributor Author

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',
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@@ -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.
Copy link
Contributor

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.

Copy link
Contributor Author

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');
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

@godofredoc godofredoc changed the base branch from master to main January 6, 2022 22:53
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a 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
Copy link
Contributor

@hellohuanlin hellohuanlin left a 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];
Copy link
Contributor

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.

Copy link
Contributor Author

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];
Copy link
Contributor

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.

Copy link
Contributor

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:]

Copy link
Contributor Author

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.

@zuvola zuvola requested a review from hellohuanlin March 16, 2022 06:07
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a 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;
Copy link
Contributor

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.)

Copy link
Contributor Author

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
Copy link
Contributor

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`.
Copy link
Contributor

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.

@zuvola zuvola requested a review from stuartmorgan-g March 23, 2022 05:08
Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a 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.
Copy link
Contributor

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.)

Copy link
Contributor Author

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
Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@renanyoy
Copy link

is it fixed in flutter 3.0 ?

@jmagman
Copy link
Member

jmagman commented May 31, 2022

This was fixed in camera plugin version 0.9.4+18, not a version of Flutter.

@zuvola zuvola deleted the fix_ios_streaming branch June 1, 2022 00:11
@renanyoy
Copy link

renanyoy commented Jun 1, 2022

ok, I have still a crash with the last version on iOS (it works on android)

flutter: |L2K|camera initialized
Initialized TensorFlow Lite runtime.
INFO: Initialized TensorFlow Lite runtime.
* thread #59, queue = 'ProviderImageSurfaceCacheQueue', stop reason = EXC_BAD_ACCESS (code=1, address=0x2a9598000)
libsystem_platform.dylib`_platform_memmove:
->  0x22b44fb0c <+188>: ldnp   q0, q1, [x1]
    0x22b44fb10 <+192>: add    x1, x1, #0x20
    0x22b44fb14 <+196>: subs   x2, x2, #0x20
    0x22b44fb18 <+200>: b.hi   0x22b44fb04               ; <+180>

edit: seems more related to googleMLKit addon..

@AnimeshHestabit
Copy link

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.'
*** First throw call stack:
(0x185349cac 0x19c3b8758 0x18def7a48 0x18def516c 0x18defa8ec 0x10383851c 0x1a7e897ac 0x1a7e74a20 0x1a80a79c8 0x1a823fc3c 0x1850162f0 0x184fb89c8 0x184fca304 0x184fbc41c 0x184fbcff0 0x184fc6ae4 0x1deccef38 0x1decceaa4)
libc++abi: terminating with uncaught exception of type NSException

  • thread Fix workaround for failing dynamic check in Xcode 7/sdk version 9. #25, queue = 'io.flutter.camera.captureSessionQueue', stop reason = signal SIGABRT
    frame #0: 0x00000001be89ea48 libsystem_kernel.dylib__pthread_kill + 8 libsystem_kernel.dylib__pthread_kill:
    -> 0x1be89ea48 <+8>: b.lo 0x1be89ea64 ; <+36>
    0x1be89ea4c <+12>: stp x29, x30, [sp, #-0x10]!
    0x1be89ea50 <+16>: mov x29, sp
    0x1be89ea54 <+20>: bl 0x1be89a6d0 ; cerror_nocancel
    Target 0: (Runner) stopped.
    Lost connection to device.

@stuartmorgan-g
Copy link
Contributor

The place to report new crashes is the issue tracker; this is a fix for an unrelated crash.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes p: camera platform-ios waiting for tree to go green (Use "autosubmit") This PR is approved and tested, but waiting for the tree to be green to land.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

iOS app crashes when camera is streaming images
7 participants