Skip to content

Commit 34948d1

Browse files
[video_player] Improve KVO handling on iOS (#9718)
This improves the KVO handling in `FVPVideoPlayer` in several ways: - Eliminates the use of methods to do KVO registration and unregistration, since that involved calling `self` during init and dealloc, which is a dangerous anti-pattern. - Eliminates `NSKeyValueObservingOptionInitial` from the registration, as this was non-obviously creating a call to self, within init, for every registration. In practice, none of them were necessary from what I could determine: - The handlers for most keys just collect information to send to the event listener, but there is no event listener during init (it's set just after), so those were expensive no-ops. - The handlers related to initialization require the item to be in the `AVPlayerItemStatusReadyToPlay` state, and items start in `AVPlayerItemStatusUnknown`, so the initial state call won't do anything useful. - Stores a dictionary of registrations, which is used for unregistration, so that it's impossible to add a new key to the observation set but forget to unregister it. Previously the two lists of strings had to be kept in sync. New static functions (to avoid `self` calls) process those lists. - Clears the list after unregistering to avoid double-unregistration locally to the class, instead of requiring a subclass to contain a workaround to avoid problems in the superclass code. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent e56761e commit 34948d1

File tree

4 files changed

+109
-74
lines changed

4 files changed

+109
-74
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.8.3
2+
3+
* Removes calls to self from init and dealloc, for maintainability.
4+
15
## 2.8.2
26

37
* Restructure internals of Dart notification of video player events.

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -119,14 +119,6 @@ - (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable)
119119
}
120120

121121
- (void)dispose {
122-
// This check prevents the crash caused by removing the KVO observers twice.
123-
// When performing a Hot Restart, the leftover players are disposed once directly
124-
// by [FVPVideoPlayerPlugin initialize:] method and then disposed again by
125-
// [FVPVideoPlayer onTextureUnregistered:] call leading to possible over-release.
126-
if (self.disposed) {
127-
return;
128-
}
129-
130122
[super dispose];
131123

132124
[self.playerLayer removeFromSuperlayer];

packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m

Lines changed: 104 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,64 @@
1616
static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext;
1717
static void *rateContext = &rateContext;
1818

19-
@implementation FVPVideoPlayer
19+
/// Registers KVO observers on 'object' for each entry in 'observations', which must be a
20+
/// dictionary mapping KVO keys to NSValue-wrapped context pointers.
21+
///
22+
/// This does not call any methods on 'observer', so is safe to call from 'observer's init.
23+
static void FVPRegisterKeyValueObservers(NSObject *observer,
24+
NSDictionary<NSString *, NSValue *> *observations,
25+
NSObject *target) {
26+
// It is important not to use NSKeyValueObservingOptionInitial here, because that will cause
27+
// synchronous calls to 'observer', violating the requirement that this method does not call its
28+
// methods. If there are use cases for specific pieces of initial state, those should be handled
29+
// explicitly by the caller, rather than by adding initial-state KVO notifications here.
30+
for (NSString *key in observations) {
31+
[target addObserver:observer
32+
forKeyPath:key
33+
options:NSKeyValueObservingOptionNew
34+
context:observations[key].pointerValue];
35+
}
36+
}
37+
38+
/// Registers KVO observers on 'object' for each entry in 'observations', which must be a
39+
/// dictionary mapping KVO keys to NSValue-wrapped context pointers.
40+
///
41+
/// This should only be called to balance calls to FVPRegisterKeyValueObservers, as it is an
42+
/// error to try to remove observers that are not currently set.
43+
///
44+
/// This does not call any methods on 'observer', so is safe to call from 'observer's dealloc.
45+
static void FVPRemoveKeyValueObservers(NSObject *observer,
46+
NSDictionary<NSString *, NSValue *> *observations,
47+
NSObject *target) {
48+
for (NSString *key in observations) {
49+
[target removeObserver:observer forKeyPath:key];
50+
}
51+
}
52+
53+
/// Returns a mapping of KVO keys to NSValue-wrapped observer context pointers for observations that
54+
/// should be set for AVPlayer instances.
55+
static NSDictionary<NSString *, NSValue *> *FVPGetPlayerObservations(void) {
56+
return @{
57+
@"rate" : [NSValue valueWithPointer:rateContext],
58+
};
59+
}
60+
61+
/// Returns a mapping of KVO keys to NSValue-wrapped observer context pointers for observations that
62+
/// should be set for AVPlayerItem instances.
63+
static NSDictionary<NSString *, NSValue *> *FVPGetPlayerItemObservations(void) {
64+
return @{
65+
@"loadedTimeRanges" : [NSValue valueWithPointer:timeRangeContext],
66+
@"status" : [NSValue valueWithPointer:statusContext],
67+
@"presentationSize" : [NSValue valueWithPointer:presentationSizeContext],
68+
@"duration" : [NSValue valueWithPointer:durationContext],
69+
@"playbackLikelyToKeepUp" : [NSValue valueWithPointer:playbackLikelyToKeepUpContext],
70+
};
71+
}
72+
73+
@implementation FVPVideoPlayer {
74+
// Whether or not player and player item listeners have ever been registered.
75+
BOOL _listenersRegistered;
76+
}
2077

2178
- (instancetype)initWithURL:(NSURL *)url
2279
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
@@ -82,66 +139,57 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
82139
};
83140
_videoOutput = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes];
84141

85-
[self addObserversForItem:item player:_player];
86-
87142
[asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] completionHandler:assetCompletionHandler];
88143

89144
return self;
90145
}
91146

92147
- (void)dealloc {
93-
if (!_disposed) {
94-
[self removeKeyValueObservers];
148+
if (_listenersRegistered && !_disposed) {
149+
// If dispose was never called for some reason, remove observers to prevent crashes.
150+
FVPRemoveKeyValueObservers(self, FVPGetPlayerItemObservations(), _player.currentItem);
151+
FVPRemoveKeyValueObservers(self, FVPGetPlayerObservations(), _player);
95152
}
96153
}
97154

98155
- (void)dispose {
156+
// In some hot restart scenarios, dispose can be called twice, so no-op after the first time.
157+
if (_disposed) {
158+
return;
159+
}
99160
_disposed = YES;
100-
[self removeKeyValueObservers];
161+
162+
if (_listenersRegistered) {
163+
[[NSNotificationCenter defaultCenter] removeObserver:self];
164+
FVPRemoveKeyValueObservers(self, FVPGetPlayerItemObservations(), self.player.currentItem);
165+
FVPRemoveKeyValueObservers(self, FVPGetPlayerObservations(), self.player);
166+
}
101167

102168
[self.player replaceCurrentItemWithPlayerItem:nil];
103-
[[NSNotificationCenter defaultCenter] removeObserver:self];
104169

105170
if (_onDisposed) {
106171
_onDisposed();
107172
}
108173
[self.eventListener videoPlayerWasDisposed];
109174
}
110175

111-
- (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player {
112-
[item addObserver:self
113-
forKeyPath:@"loadedTimeRanges"
114-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
115-
context:timeRangeContext];
116-
[item addObserver:self
117-
forKeyPath:@"status"
118-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
119-
context:statusContext];
120-
[item addObserver:self
121-
forKeyPath:@"presentationSize"
122-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
123-
context:presentationSizeContext];
124-
[item addObserver:self
125-
forKeyPath:@"duration"
126-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
127-
context:durationContext];
128-
[item addObserver:self
129-
forKeyPath:@"playbackLikelyToKeepUp"
130-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
131-
context:playbackLikelyToKeepUpContext];
132-
133-
// Add observer to AVPlayer instead of AVPlayerItem since the AVPlayerItem does not have a "rate"
134-
// property
135-
[player addObserver:self
136-
forKeyPath:@"rate"
137-
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
138-
context:rateContext];
139-
140-
// Add an observer that will respond to itemDidPlayToEndTime
141-
[[NSNotificationCenter defaultCenter] addObserver:self
142-
selector:@selector(itemDidPlayToEndTime:)
143-
name:AVPlayerItemDidPlayToEndTimeNotification
144-
object:item];
176+
- (void)setEventListener:(NSObject<FVPVideoEventListener> *)eventListener {
177+
_eventListener = eventListener;
178+
// The first time an event listener is set, set up video event listeners to relay status changes
179+
// changes to the event listener.
180+
if (eventListener && !_listenersRegistered) {
181+
AVPlayerItem *item = self.player.currentItem;
182+
// If the item is already ready to play, ensure that the intialized event is sent first.
183+
[self reportStatusForPlayerItem:item];
184+
// Set up all necessary observers to report video events.
185+
FVPRegisterKeyValueObservers(self, FVPGetPlayerItemObservations(), item);
186+
FVPRegisterKeyValueObservers(self, FVPGetPlayerObservations(), _player);
187+
[[NSNotificationCenter defaultCenter] addObserver:self
188+
selector:@selector(itemDidPlayToEndTime:)
189+
name:AVPlayerItemDidPlayToEndTimeNotification
190+
object:item];
191+
_listenersRegistered = YES;
192+
}
145193
}
146194

147195
- (void)itemDidPlayToEndTime:(NSNotification *)notification {
@@ -228,17 +276,7 @@ - (void)observeValueForKeyPath:(NSString *)path
228276
[self.eventListener videoPlayerDidUpdateBufferRegions:values];
229277
} else if (context == statusContext) {
230278
AVPlayerItem *item = (AVPlayerItem *)object;
231-
switch (item.status) {
232-
case AVPlayerItemStatusFailed:
233-
[self sendFailedToLoadVideoEvent];
234-
break;
235-
case AVPlayerItemStatusUnknown:
236-
break;
237-
case AVPlayerItemStatusReadyToPlay:
238-
[item addOutput:_videoOutput];
239-
[self reportInitializedIfReadyToPlay];
240-
break;
241-
}
279+
[self reportStatusForPlayerItem:item];
242280
} else if (context == presentationSizeContext || context == durationContext) {
243281
AVPlayerItem *item = (AVPlayerItem *)object;
244282
if (item.status == AVPlayerItemStatusReadyToPlay) {
@@ -262,6 +300,20 @@ - (void)observeValueForKeyPath:(NSString *)path
262300
}
263301
}
264302

303+
- (void)reportStatusForPlayerItem:(AVPlayerItem *)item {
304+
switch (item.status) {
305+
case AVPlayerItemStatusFailed:
306+
[self sendFailedToLoadVideoEvent];
307+
break;
308+
case AVPlayerItemStatusUnknown:
309+
break;
310+
case AVPlayerItemStatusReadyToPlay:
311+
[item addOutput:_videoOutput];
312+
[self reportInitializedIfReadyToPlay];
313+
break;
314+
}
315+
}
316+
265317
- (void)updatePlayingState {
266318
if (!_isInitialized) {
267319
return;
@@ -436,17 +488,4 @@ - (int64_t)duration {
436488
return FVPCMTimeToMillis([[[_player currentItem] asset] duration]);
437489
}
438490

439-
/// Removes all key-value observers set up for the player.
440-
///
441-
/// This is called from dealloc, so must not use any methods on self.
442-
- (void)removeKeyValueObservers {
443-
AVPlayerItem *currentItem = _player.currentItem;
444-
[currentItem removeObserver:self forKeyPath:@"status"];
445-
[currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
446-
[currentItem removeObserver:self forKeyPath:@"presentationSize"];
447-
[currentItem removeObserver:self forKeyPath:@"duration"];
448-
[currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
449-
[_player removeObserver:self forKeyPath:@"rate"];
450-
}
451-
452491
@end

packages/video_player/video_player_avfoundation/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_avfoundation
22
description: iOS and macOS implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.8.2
5+
version: 2.8.3
66

77
environment:
88
sdk: ^3.6.0

0 commit comments

Comments
 (0)