[video_player] Remove KVO observer on AVPlayerItem on iOS (#4683)

diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 58fa084..dfd4a31 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.2.14
+
+* Removes KVO observer on AVPlayerItem on iOS.
+
 ## 2.2.13
 
 * Fixes persisting of hasError even after successful initialize.
diff --git a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m
index deea833..90c7dc2 100644
--- a/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m
+++ b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m
@@ -2,34 +2,37 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+@import AVFoundation;
 @import video_player;
 @import XCTest;
 
 #import <OCMock/OCMock.h>
 
+@interface FLTVideoPlayer : NSObject
+@property(readonly, nonatomic) AVPlayer *player;
+@end
+
+@interface FLTVideoPlayerPlugin (Test) <FLTVideoPlayerApi>
+@property(readonly, strong, nonatomic)
+    NSMutableDictionary<NSNumber *, FLTVideoPlayer *> *playersByTextureId;
+@end
+
 @interface VideoPlayerTests : XCTestCase
 @end
 
 @implementation VideoPlayerTests
 
-- (void)testPlugin {
-  FLTVideoPlayerPlugin *plugin = [[FLTVideoPlayerPlugin alloc] init];
-  XCTAssertNotNil(plugin);
-}
-
 - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry {
   NSObject<FlutterTextureRegistry> *mockTextureRegistry =
       OCMProtocolMock(@protocol(FlutterTextureRegistry));
   NSObject<FlutterPluginRegistry> *registry =
       (NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
   NSObject<FlutterPluginRegistrar> *registrar =
-      [registry registrarForPlugin:@"TEST_FLTVideoPlayerPlugin"];
+      [registry registrarForPlugin:@"SeekToInvokestextureFrameAvailable"];
   NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
   OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
-  [FLTVideoPlayerPlugin registerWithRegistrar:partialRegistrar];
-  FLTVideoPlayerPlugin<FLTVideoPlayerApi> *videoPlayerPlugin =
-      (FLTVideoPlayerPlugin<FLTVideoPlayerApi> *)[[FLTVideoPlayerPlugin alloc]
-          initWithRegistrar:partialRegistrar];
+  FLTVideoPlayerPlugin *videoPlayerPlugin =
+      (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar];
   FLTPositionMessage *message = [[FLTPositionMessage alloc] init];
   message.textureId = @101;
   message.position = @0;
@@ -38,4 +41,33 @@
   OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId.intValue]);
 }
 
+- (void)testDeregistersFromPlayer {
+  NSObject<FlutterPluginRegistry> *registry =
+      (NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
+  NSObject<FlutterPluginRegistrar> *registrar =
+      [registry registrarForPlugin:@"testDeregistersFromPlayer"];
+  FLTVideoPlayerPlugin *videoPlayerPlugin =
+      (FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
+
+  FlutterError *error;
+  [videoPlayerPlugin initialize:&error];
+  XCTAssertNil(error);
+
+  FLTCreateMessage *create = [[FLTCreateMessage alloc] init];
+  create.uri = @"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
+  FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
+  XCTAssertNil(error);
+  XCTAssertNotNil(textureMessage);
+  FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
+  XCTAssertNotNil(player);
+  AVPlayer *avPlayer = player.player;
+
+  [videoPlayerPlugin dispose:textureMessage error:&error];
+  XCTAssertEqual(videoPlayerPlugin.playersByTextureId.count, 0);
+  XCTAssertNil(error);
+
+  [self keyValueObservingExpectationForObject:avPlayer keyPath:@"currentItem" expectedValue:nil];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
 @end
diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
index d5bea17..a67e371 100644
--- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
+++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
@@ -37,17 +37,13 @@
 @property(nonatomic) FlutterEventChannel* eventChannel;
 @property(nonatomic) FlutterEventSink eventSink;
 @property(nonatomic) CGAffineTransform preferredTransform;
-@property(nonatomic, readonly) bool disposed;
-@property(nonatomic, readonly) bool isPlaying;
-@property(nonatomic) bool isLooping;
-@property(nonatomic, readonly) bool isInitialized;
+@property(nonatomic, readonly) BOOL disposed;
+@property(nonatomic, readonly) BOOL isPlaying;
+@property(nonatomic) BOOL isLooping;
+@property(nonatomic, readonly) BOOL isInitialized;
 - (instancetype)initWithURL:(NSURL*)url
                frameUpdater:(FLTFrameUpdater*)frameUpdater
                 httpHeaders:(NSDictionary<NSString*, NSString*>*)headers;
-- (void)play;
-- (void)pause;
-- (void)setIsLooping:(bool)isLooping;
-- (void)updatePlayingState;
 @end
 
 static void* timeRangeContext = &timeRangeContext;
@@ -114,7 +110,7 @@
 
 const int64_t TIME_UNSET = -9223372036854775807;
 
-static inline int64_t FLTCMTimeToMillis(CMTime time) {
+NS_INLINE int64_t FLTCMTimeToMillis(CMTime time) {
   // When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android.
   // Fixes https://github.com/flutter/flutter/issues/48670
   if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET;
@@ -122,14 +118,14 @@
   return time.value * 1000 / time.timescale;
 }
 
-static inline CGFloat radiansToDegrees(CGFloat radians) {
+NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
   // Input range [-pi, pi] or [-180, 180]
   CGFloat degrees = GLKMathRadiansToDegrees((float)radians);
   if (degrees < 0) {
     // Convert -90 to 270 and -180 to 180
     return degrees + 360;
   }
-  // Output degrees in between [0, 360[
+  // Output degrees in between [0, 360]
   return degrees;
 };
 
@@ -217,9 +213,6 @@
 - (instancetype)initWithPlayerItem:(AVPlayerItem*)item frameUpdater:(FLTFrameUpdater*)frameUpdater {
   self = [super init];
   NSAssert(self, @"super init cannot be nil");
-  _isInitialized = false;
-  _isPlaying = false;
-  _disposed = false;
 
   AVAsset* asset = [item asset];
   void (^assetCompletionHandler)(void) = ^{
@@ -352,7 +345,7 @@
       return;
     }
 
-    _isInitialized = true;
+    _isInitialized = YES;
     _eventSink(@{
       @"event" : @"initialized",
       @"duration" : @([self duration]),
@@ -363,12 +356,12 @@
 }
 
 - (void)play {
-  _isPlaying = true;
+  _isPlaying = YES;
   [self updatePlayingState];
 }
 
 - (void)pause {
-  _isPlaying = false;
+  _isPlaying = NO;
   [self updatePlayingState];
 }
 
@@ -389,7 +382,7 @@
        toleranceAfter:kCMTimeZero];
 }
 
-- (void)setIsLooping:(bool)isLooping {
+- (void)setIsLooping:(BOOL)isLooping {
   _isLooping = isLooping;
 }
 
@@ -457,22 +450,18 @@
 /// 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 {
-  _disposed = true;
+  _disposed = YES;
   [_displayLink invalidate];
-  [[_player currentItem] removeObserver:self forKeyPath:@"status" context:statusContext];
-  [[_player currentItem] removeObserver:self
-                             forKeyPath:@"loadedTimeRanges"
-                                context:timeRangeContext];
-  [[_player currentItem] removeObserver:self
-                             forKeyPath:@"playbackLikelyToKeepUp"
-                                context:playbackLikelyToKeepUpContext];
-  [[_player currentItem] removeObserver:self
-                             forKeyPath:@"playbackBufferEmpty"
-                                context:playbackBufferEmptyContext];
-  [[_player currentItem] removeObserver:self
-                             forKeyPath:@"playbackBufferFull"
-                                context:playbackBufferFullContext];
-  [_player replaceCurrentItemWithPlayerItem:nil];
+  AVPlayerItem* currentItem = self.player.currentItem;
+  [currentItem removeObserver:self forKeyPath:@"status"];
+  [currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
+  [currentItem removeObserver:self forKeyPath:@"presentationSize"];
+  [currentItem removeObserver:self forKeyPath:@"duration"];
+  [currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
+  [currentItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
+  [currentItem removeObserver:self forKeyPath:@"playbackBufferFull"];
+
+  [self.player replaceCurrentItemWithPlayerItem:nil];
   [[NSNotificationCenter defaultCenter] removeObserver:self];
 }
 
@@ -486,7 +475,8 @@
 @interface FLTVideoPlayerPlugin () <FLTVideoPlayerApi>
 @property(readonly, weak, nonatomic) NSObject<FlutterTextureRegistry>* registry;
 @property(readonly, weak, nonatomic) NSObject<FlutterBinaryMessenger>* messenger;
-@property(readonly, strong, nonatomic) NSMutableDictionary* players;
+@property(readonly, strong, nonatomic)
+    NSMutableDictionary<NSNumber*, FLTVideoPlayer*>* playersByTextureId;
 @property(readonly, strong, nonatomic) NSObject<FlutterPluginRegistrar>* registrar;
 @end
 
@@ -503,16 +493,13 @@
   _registry = [registrar textures];
   _messenger = [registrar messenger];
   _registrar = registrar;
-  _players = [NSMutableDictionary dictionaryWithCapacity:1];
+  _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1];
   return self;
 }
 
 - (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
-  for (NSNumber* textureId in _players.allKeys) {
-    FLTVideoPlayer* player = _players[textureId];
-    [player disposeSansEventChannel];
-  }
-  [_players removeAllObjects];
+  [self.playersByTextureId.allValues makeObjectsPerformSelector:@selector(disposeSansEventChannel)];
+  [self.playersByTextureId removeAllObjects];
   // TODO(57151): This should be commented out when 57151's fix lands on stable.
   // This is the correct behavior we never did it in the past and the engine
   // doesn't currently support it.
@@ -521,7 +508,7 @@
 
 - (FLTTextureMessage*)onPlayerSetup:(FLTVideoPlayer*)player
                        frameUpdater:(FLTFrameUpdater*)frameUpdater {
-  int64_t textureId = [_registry registerTexture:player];
+  int64_t textureId = [self.registry registerTexture:player];
   frameUpdater.textureId = textureId;
   FlutterEventChannel* eventChannel = [FlutterEventChannel
       eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%lld",
@@ -529,7 +516,7 @@
            binaryMessenger:_messenger];
   [eventChannel setStreamHandler:player];
   player.eventChannel = eventChannel;
-  _players[@(textureId)] = player;
+  self.playersByTextureId[@(textureId)] = player;
   FLTTextureMessage* result = [[FLTTextureMessage alloc] init];
   result.textureId = @(textureId);
   return result;
@@ -539,11 +526,12 @@
   // Allow audio playback when the Ring/Silent switch is set to silent
   [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
 
-  for (NSNumber* textureId in _players) {
-    [_registry unregisterTexture:[textureId unsignedIntegerValue]];
-    [_players[textureId] dispose];
-  }
-  [_players removeAllObjects];
+  [self.playersByTextureId
+      enumerateKeysAndObjectsUsingBlock:^(NSNumber* textureId, FLTVideoPlayer* player, BOOL* stop) {
+        [self.registry unregisterTexture:textureId.unsignedIntegerValue];
+        [player dispose];
+      }];
+  [self.playersByTextureId removeAllObjects];
 }
 
 - (FLTTextureMessage*)create:(FLTCreateMessage*)input error:(FlutterError**)error {
@@ -570,9 +558,9 @@
 }
 
 - (void)dispose:(FLTTextureMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
-  [_registry unregisterTexture:input.textureId.intValue];
-  [_players removeObjectForKey:input.textureId];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
+  [self.registry unregisterTexture:input.textureId.intValue];
+  [self.playersByTextureId removeObjectForKey:input.textureId];
   // If the Flutter contains https://github.com/flutter/engine/pull/12695,
   // the `player` is disposed via `onTextureUnregistered` at the right time.
   // Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the
@@ -592,46 +580,46 @@
 }
 
 - (void)setLooping:(FLTLoopingMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
-  [player setIsLooping:[input.isLooping boolValue]];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
+  player.isLooping = input.isLooping.boolValue;
 }
 
 - (void)setVolume:(FLTVolumeMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
-  [player setVolume:[input.volume doubleValue]];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
+  [player setVolume:input.volume.doubleValue];
 }
 
 - (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
-  [player setPlaybackSpeed:[input.speed doubleValue]];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
+  [player setPlaybackSpeed:input.speed.doubleValue];
 }
 
 - (void)play:(FLTTextureMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
   [player play];
 }
 
 - (FLTPositionMessage*)position:(FLTTextureMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
   FLTPositionMessage* result = [[FLTPositionMessage alloc] init];
   result.position = @([player position]);
   return result;
 }
 
 - (void)seekTo:(FLTPositionMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
-  [player seekTo:[input.position intValue]];
-  [_registry textureFrameAvailable:input.textureId.intValue];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
+  [player seekTo:input.position.intValue];
+  [self.registry textureFrameAvailable:input.textureId.intValue];
 }
 
 - (void)pause:(FLTTextureMessage*)input error:(FlutterError**)error {
-  FLTVideoPlayer* player = _players[input.textureId];
+  FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
   [player pause];
 }
 
 - (void)setMixWithOthers:(FLTMixWithOthersMessage*)input
                    error:(FlutterError* _Nullable __autoreleasing*)error {
-  if ([input.mixWithOthers boolValue]) {
+  if (input.mixWithOthers.boolValue) {
     [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
                                      withOptions:AVAudioSessionCategoryOptionMixWithOthers
                                            error:nil];
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index 98c9b95..0260b84 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -3,7 +3,7 @@
   widgets on Android, iOS, and web.
 repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
-version: 2.2.13
+version: 2.2.14
 
 environment:
   sdk: ">=2.14.0 <3.0.0"