Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[video_player] Fix iOS crash with multiple players #4202

Merged
merged 10 commits into from
Jul 18, 2023

Conversation

turekj
Copy link
Contributor

@turekj turekj commented Jun 12, 2023

This PR fixes crash in video_player package on iOS.

flutter/flutter#124937

Detailed description

I can observe the crash when displaying a couple of the different players (different URLs) inside a ListView. The crash happens inside of AVFoundation framework:

[AVPlayer _createAndConfigureFigPlayerWithType:completionHandler:]

In order to debug the issue, I ran the application using the plugin with Zombie Objects inspection turned on. The Zombie Objects reports the following issue:

*** -[FLTVideoPlayer retainWeakReference]: message sent to deallocated instance 0x6030009b2e10

This, in conjunction with the NSKeyValueWillChange line present in the stack trace led me to believe, that culprit is sending a KVO notification to the FLTVideoPlayer instance that's deallocated.

Next, I examined the plugin code and identified one property that doesn't have the KVO removed. In addObserversForItem:player: method in FLTVideoPlayerPlugin.m:

  [player addObserver:self
           forKeyPath:@"rate"
              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
              context:rateContext];

The observer for @"rate" is never cleaned up. To be entirely sure that the issue comes from KVO that's not cleaned up, I've added the following class:

@implementation EmptyObserver

- (void)observeValueForKeyPath:(NSString *)path
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {}

@end

and registered the observation as follows (notice that EmptyObserver is never retained and deallocated instantly):

  [player addObserver:[[EmptyObserver alloc] init]
           forKeyPath:@"rate"
              options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
              context:rateContext];

The exception I got seems to be matching the underlying issue:

*** -[EmptyObserver retainWeakReference]: message sent to deallocated instance 0x6020001ceb70

This means the fix for the issue is to add the following to disposeSansEventChannel method:

[self.player removeObserver:self forKeyPath:@"rate"];

After applying the patch, I can no longer crash the player.

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/packages repo does use dart format.)
  • I signed the [CLA].
  • The title of the PR starts with the name of the package 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.

@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 (don't just cc him here, he won't see it! He's on Discord!).

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

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

can you explain in the PR description why the crash happens and how this fixes the crash?


[self.player replaceCurrentItemWithPlayerItem:nil];
_player = nil;
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this required?

Copy link
Contributor Author

@turekj turekj Jun 14, 2023

Choose a reason for hiding this comment

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

After applying the original fix (removing the observer), I started observing crashes when doing Hot Reload with Flutter debugger attached on the screen displaying the player. The code crashes at:

[self.player removeObserver:self forKeyPath:@"rate"]

The immediate reason for the crash is that the player is no longer KVO subscribed to the property rate, therefore removeObserver call crashes. I've added additional logging to pinpoint why that happens. It turns out that in the flow described, the disposeSansEventChannel is called twice in a row for the same player.

The issue is not present in the original code, because of this line:

[self.player replaceCurrentItemWithPlayerItem:nil]

Upon a first call to the disposeSansEventChannel method, the self.player.currentItem is unassigned. On the second call, the currentItem is nil and therefore any of the removeObserver calls become no-op.

I applied the same solution to the player.

Copy link
Contributor

Choose a reason for hiding this comment

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

when it's called twice, should we short circuit up front?

- (void)dispose... {
  if (!_player) {
    return;
  } 
  ... 
}

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'll short circuit it with:

- (void)disposeSansEventChannel {
  if (_disposed) {
    return;
  }
  _disposed = YES;
// ...
}

as _disposed is already set there.

Copy link
Contributor

Choose a reason for hiding this comment

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

makes sense. Can you also add some comments here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@hellohuanlin I've updated the code according to our discussion. I've added a comment on the short circuit method.

I don't think removing the observer requires additional comments as the API specifies to clear any observations you've subscribed to in the first place. LMK if you'd add anything else, please 🙏

@turekj
Copy link
Contributor Author

turekj commented Jun 14, 2023

@hellohuanlin I've added the detailed description in the PR (as requested).

I'll update the code as requested and provide some additional comments.

@hellohuanlin
Copy link
Contributor

I'll update the code as requested and provide some additional comments.

Oh i meant comment in the code. It would be helpful for future readers.

@turekj turekj requested a review from hellohuanlin June 16, 2023 14:44
@@ -519,6 +519,14 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
/// is useful for the case where the Engine is in the process of deconstruction
/// so the channel is going to die or is already dead.
- (void)disposeSansEventChannel {
// This check prevents the crash caused by removing the KVO observers twice.
// When performing a Hot Restart, the leftover players are disposed directly
// and also receive onTextureUnregistered: callback leading to possible
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: just wanna be clearer

"the leftover players are disposed twice - once directly (from which function?), and once when receiving onTextureUnregistered callback"

Copy link
Contributor Author

@turekj turekj Jul 14, 2023

Choose a reason for hiding this comment

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

@hellohuanlin I've updated the comment yet again to name the calling methods!

@stuartmorgan
Copy link
Contributor

  • 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 added new tests to check the change I am making, or this PR is [test-exempt].

@turekj Could you update the PR to add these missing components? All of these elements are required before the PR can be fully reviewed and landed.

@stuartmorgan
Copy link
Contributor

From PR triage: @turekj Are you still planning on updating this per the feedback above?

@turekj turekj force-pushed the fix-ios-video-player-crash branch from 345f3d1 to 2ba7752 Compare July 14, 2023 10:43
@turekj
Copy link
Contributor Author

turekj commented Jul 14, 2023

@hellohuanlin I've updated the comments so that they're clearer ✅

@stuartmorgan I've added a changelog entry, bumped the package number, and also implemented the test cases for both of the changes I made.

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.

Some feedbacks mainly on unit tests

}

// [FLTVideoPlayerPlugin dispose:error:] selector is dispatching the [FLTVideoPlayer dispose] call
// with a 1-second delay keeping a strong reference to the player. The polling ensures the player
Copy link
Contributor

Choose a reason for hiding this comment

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

what does this 1 sec delay come from?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The [FLTVideoPlayerPlugin dispose:error:] has this piece of code:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
               dispatch_get_main_queue(), ^{
                 if (!player.disposed) {
                   [player dispose];
                 }
               });

} while ([[NSDate date] timeIntervalSinceNow] < [end timeIntervalSinceNow]);

if (condition()) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

is this check necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not valid anymore

@@ -439,4 +546,22 @@ - (void)validateTransformFixForOrientation:(UIImageOrientation)orientation {
XCTAssertEqual(t.ty, expectY);
}

- (void)waitForCondition:(BOOL (^)(void))condition withTimeout:(NSTimeInterval)timeout {
Copy link
Contributor

Choose a reason for hiding this comment

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

i think you can just use NSPredicate and expectation for this kind of stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great idea, I've refactored the code ✅

@@ -519,6 +519,14 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments
/// is useful for the case where the Engine is in the process of deconstruction
/// so the channel is going to die or is already dead.
- (void)disposeSansEventChannel {
// This check prevents the crash caused by removing the KVO observers twice.
// When performing a Hot Restart, the leftover players are disposed directly
// by [FLTVideoPlayerPlugin initialize:] method and also receive
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: "the leftover players are disposed once directly by ... method and then disposed again when receiving ..."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated the wording ✅

@turekj
Copy link
Contributor Author

turekj commented Jul 14, 2023

@hellohuanlin implemented all rounds of feedback ✅

@stuartmorgan
Copy link
Contributor

@turekj Could you resolve the conflicts that have shown up? Once that's done this can be landed.

@turekj
Copy link
Contributor Author

turekj commented Jul 18, 2023

@stuartmorgan conflicts resolved ✅

@stuartmorgan stuartmorgan added autosubmit Merge PR when tree becomes green via auto submit App and removed needs tests labels Jul 18, 2023
@stuartmorgan
Copy link
Contributor

Thanks!

@auto-submit auto-submit bot merged commit 3e8b813 into flutter:main Jul 18, 2023
73 checks passed
engine-flutter-autoroll added a commit to engine-flutter-autoroll/flutter that referenced this pull request Jul 18, 2023
auto-submit bot pushed a commit to flutter/flutter that referenced this pull request Jul 18, 2023
flutter/packages@6889cca...3e8b813

2023-07-18 jkbturek@gmail.com [video_player] Fix iOS crash with multiple players (flutter/packages#4202)
2023-07-17 stuartmorgan@google.com [pigeon] Enable Android emulator tests in CI (flutter/packages#4484)
2023-07-17 defuncart@gmail.com [video_player] Add optional web options [Platform interface] (flutter/packages#4433)
2023-07-17 rexios@rexios.dev [google_maps_flutter_platform_interface] Platform interface changes for #3258 (flutter/packages#4478)
2023-07-17 yan.outside@gmail.com [video_player] fix: add missing isPlaybackLikelyToKeepUp check. (flutter/packages#3826)
2023-07-17 43054281+camsim99@users.noreply.github.com [camerax] Add flash configuration for image capture (flutter/packages#3800)
2023-07-17 stuartmorgan@google.com Remove `equatable` and `xml` allowances (flutter/packages#4489)
2023-07-17 stuartmorgan@google.com [ci] Switch Linux platform tests to LUCI (flutter/packages#4479)
2023-07-17 stuartmorgan@google.com [ci] Adjust bot configurations (flutter/packages#4485)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com,rmistry@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
LouiseHsu pushed a commit to LouiseHsu/flutter that referenced this pull request Jul 31, 2023
flutter/packages@6889cca...3e8b813

2023-07-18 jkbturek@gmail.com [video_player] Fix iOS crash with multiple players (flutter/packages#4202)
2023-07-17 stuartmorgan@google.com [pigeon] Enable Android emulator tests in CI (flutter/packages#4484)
2023-07-17 defuncart@gmail.com [video_player] Add optional web options [Platform interface] (flutter/packages#4433)
2023-07-17 rexios@rexios.dev [google_maps_flutter_platform_interface] Platform interface changes for flutter#3258 (flutter/packages#4478)
2023-07-17 yan.outside@gmail.com [video_player] fix: add missing isPlaybackLikelyToKeepUp check. (flutter/packages#3826)
2023-07-17 43054281+camsim99@users.noreply.github.com [camerax] Add flash configuration for image capture (flutter/packages#3800)
2023-07-17 stuartmorgan@google.com Remove `equatable` and `xml` allowances (flutter/packages#4489)
2023-07-17 stuartmorgan@google.com [ci] Switch Linux platform tests to LUCI (flutter/packages#4479)
2023-07-17 stuartmorgan@google.com [ci] Adjust bot configurations (flutter/packages#4485)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages-flutter-autoroll
Please CC flutter-ecosystem@google.com,rmistry@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
autosubmit Merge PR when tree becomes green via auto submit App p: video_player platform-ios
Projects
None yet
4 participants