Skip to content

[video_player] Fix iOS crash with multiple players #4202

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

Merged
merged 10 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.4.9

* Fixes the iOS crash when using multiple players on the same screen.
See: https://github.com/flutter/flutter/issues/124937

## 2.4.8

* Fixes missing `isPlaybackLikelyToKeepUp` check for iOS video player `bufferingEnd` event and `bufferingStart` event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ @interface FLTVideoPlayer : NSObject <FlutterStreamHandler>
@property(readonly, nonatomic) AVPlayer *player;
@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
@property(readonly, nonatomic) int64_t position;

- (void)onTextureUnregistered:(NSObject<FlutterTexture> *)texture;
@end

@interface FLTVideoPlayerPlugin (Test) <FLTAVFoundationVideoPlayerApi>
Expand Down Expand Up @@ -442,6 +444,110 @@ - (void)testSeekToleranceWhenSeekingToEnd {
return initializationEvent;
}

// Checks whether [AVPlayer rate] KVO observations are correctly detached.
// - https://github.com/flutter/flutter/issues/124937
//
// Failing to de-register results in a crash in [AVPlayer willChangeValueForKey:].
- (void)testDoesNotCrashOnRateObservationAfterDisposal {
NSObject<FlutterPluginRegistry> *registry =
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
NSObject<FlutterPluginRegistrar> *registrar =
[registry registrarForPlugin:@"testDoesNotCrashOnRateObservationAfterDisposal"];

AVPlayer *avPlayer = nil;
__weak FLTVideoPlayer *player = nil;

// Autoreleasepool is needed to simulate conditions of FLTVideoPlayer deallocation.
@autoreleasepool {
FLTVideoPlayerPlugin *videoPlayerPlugin =
(FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];

FlutterError *error;
[videoPlayerPlugin initialize:&error];
XCTAssertNil(error);

FLTCreateMessage *create = [FLTCreateMessage
makeWithAsset:nil
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
packageName:nil
formatHint:nil
httpHeaders:@{}];
FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
XCTAssertNil(error);
XCTAssertNotNil(textureMessage);

player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
XCTAssertNotNil(player);
avPlayer = player.player;

[videoPlayerPlugin dispose:textureMessage error:&error];
XCTAssertNil(error);
}

// [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];
                 }
               });

// was truly deallocated.
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"self != nil"]
evaluatedWithObject:player
handler:nil];
[self waitForExpectationsWithTimeout:10.0 handler:nil];

[avPlayer willChangeValueForKey:@"rate"]; // No assertions needed. Lack of crash is a success.
}

// During the hot reload:
// 1. `[FLTVideoPlayer onTextureUnregistered:]` gets called.
// 2. `[FLTVideoPlayerPlugin initialize:]` gets called.
//
// Both of these methods dispatch [FLTVideoPlayer dispose] on the main thread
// leading to a possible crash when de-registering observers twice.
- (void)testHotReloadDoesNotCrash {
NSObject<FlutterPluginRegistry> *registry =
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
NSObject<FlutterPluginRegistrar> *registrar =
[registry registrarForPlugin:@"testHotReloadDoesNotCrash"];

__weak FLTVideoPlayer *player = nil;

// Autoreleasepool is needed to simulate conditions of FLTVideoPlayer deallocation.
@autoreleasepool {
FLTVideoPlayerPlugin *videoPlayerPlugin =
(FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];

FlutterError *error;
[videoPlayerPlugin initialize:&error];
XCTAssertNil(error);

FLTCreateMessage *create = [FLTCreateMessage
makeWithAsset:nil
uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"
packageName:nil
formatHint:nil
httpHeaders:@{}];
FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
XCTAssertNil(error);
XCTAssertNotNil(textureMessage);

player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
XCTAssertNotNil(player);

[player onTextureUnregistered:nil];
XCTAssertNil(error);

[videoPlayerPlugin initialize:&error];
XCTAssertNil(error);
}

// [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
// was truly deallocated.
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"self != nil"]
evaluatedWithObject:player
handler:nil];
[self waitForExpectationsWithTimeout:10.0
handler:nil]; // No assertions needed. Lack of crash is a success.
}

- (void)validateTransformFixForOrientation:(UIImageOrientation)orientation {
AVAssetTrack *track = [[FakeAVAssetTrack alloc] initWithOrientation:orientation];
CGAffineTransform t = FLTGetStandardizedTransformForTrack(track);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,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 once directly
// by [FLTVideoPlayerPlugin initialize:] method and then disposed again by
// [FLTVideoPlayer onTextureUnregistered:] call leading to possible over-release.
if (_disposed) {
return;
}

_disposed = YES;
[_playerLayer removeFromSuperlayer];
[_displayLink invalidate];
Expand All @@ -514,6 +522,7 @@ - (void)disposeSansEventChannel {
[currentItem removeObserver:self forKeyPath:@"presentationSize"];
[currentItem removeObserver:self forKeyPath:@"duration"];
[currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
[self.player removeObserver:self forKeyPath:@"rate"];

[self.player replaceCurrentItemWithPlayerItem:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_avfoundation
description: iOS implementation of the video_player plugin.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.4.8
version: 2.4.9

environment:
sdk: ">=2.18.0 <4.0.0"
Expand Down