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

Commit fd76289

Browse files
committed
[video_player] On iOS deregister KVO observer on AVPlayer
1 parent 3e4483d commit fd76289

File tree

4 files changed

+98
-74
lines changed

4 files changed

+98
-74
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.2.14
2+
3+
* Remove KVO observer on AVPlayerItem on iOS.
4+
15
## 2.2.13
26

37
* Fixes persisting of hasError even after successful initialize.

packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,37 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
@import AVFoundation;
56
@import video_player;
67
@import XCTest;
78

89
#import <OCMock/OCMock.h>
910

11+
@interface FLTVideoPlayer : NSObject
12+
@property(readonly, nonatomic) AVPlayer *player;
13+
@end
14+
15+
@interface FLTVideoPlayerPlugin (Test) <FLTVideoPlayerApi>
16+
@property(readonly, strong, nonatomic)
17+
NSMutableDictionary<NSNumber *, FLTVideoPlayer *> *playersByTextureId;
18+
@end
19+
1020
@interface VideoPlayerTests : XCTestCase
1121
@end
1222

1323
@implementation VideoPlayerTests
1424

15-
- (void)testPlugin {
16-
FLTVideoPlayerPlugin *plugin = [[FLTVideoPlayerPlugin alloc] init];
17-
XCTAssertNotNil(plugin);
18-
}
19-
2025
- (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry {
2126
NSObject<FlutterTextureRegistry> *mockTextureRegistry =
2227
OCMProtocolMock(@protocol(FlutterTextureRegistry));
2328
NSObject<FlutterPluginRegistry> *registry =
2429
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
2530
NSObject<FlutterPluginRegistrar> *registrar =
26-
[registry registrarForPlugin:@"TEST_FLTVideoPlayerPlugin"];
31+
[registry registrarForPlugin:@"SeekToInvokestextureFrameAvailable"];
2732
NSObject<FlutterPluginRegistrar> *partialRegistrar = OCMPartialMock(registrar);
2833
OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry);
29-
[FLTVideoPlayerPlugin registerWithRegistrar:partialRegistrar];
30-
FLTVideoPlayerPlugin<FLTVideoPlayerApi> *videoPlayerPlugin =
31-
(FLTVideoPlayerPlugin<FLTVideoPlayerApi> *)[[FLTVideoPlayerPlugin alloc]
32-
initWithRegistrar:partialRegistrar];
34+
FLTVideoPlayerPlugin *videoPlayerPlugin =
35+
(FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar];
3336
FLTPositionMessage *message = [[FLTPositionMessage alloc] init];
3437
message.textureId = @101;
3538
message.position = @0;
@@ -38,4 +41,33 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry {
3841
OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId.intValue]);
3942
}
4043

44+
- (void)testDeregistersFromPlayer {
45+
NSObject<FlutterPluginRegistry> *registry =
46+
(NSObject<FlutterPluginRegistry> *)[[UIApplication sharedApplication] delegate];
47+
NSObject<FlutterPluginRegistrar> *registrar =
48+
[registry registrarForPlugin:@"testDeregistersFromPlayer"];
49+
FLTVideoPlayerPlugin *videoPlayerPlugin =
50+
(FLTVideoPlayerPlugin *)[[FLTVideoPlayerPlugin alloc] initWithRegistrar:registrar];
51+
52+
FlutterError *error;
53+
[videoPlayerPlugin initialize:&error];
54+
XCTAssertNil(error);
55+
56+
FLTCreateMessage *create = [[FLTCreateMessage alloc] init];
57+
create.uri = @"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4";
58+
FLTTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&error];
59+
XCTAssertNil(error);
60+
XCTAssertNotNil(textureMessage);
61+
FLTVideoPlayer *player = videoPlayerPlugin.playersByTextureId[textureMessage.textureId];
62+
XCTAssertNotNil(player);
63+
AVPlayer *avPlayer = player.player;
64+
65+
[videoPlayerPlugin dispose:textureMessage error:&error];
66+
XCTAssertEqual(videoPlayerPlugin.playersByTextureId.count, 0);
67+
XCTAssertNil(error);
68+
69+
[self keyValueObservingExpectationForObject:avPlayer keyPath:@"currentItem" expectedValue:nil];
70+
[self waitForExpectationsWithTimeout:1 handler:nil];
71+
}
72+
4173
@end

packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m

Lines changed: 51 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,13 @@ @interface FLTVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
3737
@property(nonatomic) FlutterEventChannel* eventChannel;
3838
@property(nonatomic) FlutterEventSink eventSink;
3939
@property(nonatomic) CGAffineTransform preferredTransform;
40-
@property(nonatomic, readonly) bool disposed;
41-
@property(nonatomic, readonly) bool isPlaying;
42-
@property(nonatomic) bool isLooping;
43-
@property(nonatomic, readonly) bool isInitialized;
40+
@property(nonatomic, readonly) BOOL disposed;
41+
@property(nonatomic, readonly) BOOL isPlaying;
42+
@property(nonatomic) BOOL isLooping;
43+
@property(nonatomic, readonly) BOOL isInitialized;
4444
- (instancetype)initWithURL:(NSURL*)url
4545
frameUpdater:(FLTFrameUpdater*)frameUpdater
4646
httpHeaders:(NSDictionary<NSString*, NSString*>*)headers;
47-
- (void)play;
48-
- (void)pause;
49-
- (void)setIsLooping:(bool)isLooping;
50-
- (void)updatePlayingState;
5147
@end
5248

5349
static void* timeRangeContext = &timeRangeContext;
@@ -114,22 +110,22 @@ - (void)itemDidPlayToEndTime:(NSNotification*)notification {
114110

115111
const int64_t TIME_UNSET = -9223372036854775807;
116112

117-
static inline int64_t FLTCMTimeToMillis(CMTime time) {
113+
NS_INLINE int64_t FLTCMTimeToMillis(CMTime time) {
118114
// When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android.
119115
// Fixes https://github.com/flutter/flutter/issues/48670
120116
if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET;
121117
if (time.timescale == 0) return 0;
122118
return time.value * 1000 / time.timescale;
123119
}
124120

125-
static inline CGFloat radiansToDegrees(CGFloat radians) {
121+
NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
126122
// Input range [-pi, pi] or [-180, 180]
127123
CGFloat degrees = GLKMathRadiansToDegrees((float)radians);
128124
if (degrees < 0) {
129125
// Convert -90 to 270 and -180 to 180
130126
return degrees + 360;
131127
}
132-
// Output degrees in between [0, 360[
128+
// Output degrees in between [0, 360]
133129
return degrees;
134130
};
135131

@@ -217,9 +213,6 @@ - (CGAffineTransform)fixTransform:(AVAssetTrack*)videoTrack {
217213
- (instancetype)initWithPlayerItem:(AVPlayerItem*)item frameUpdater:(FLTFrameUpdater*)frameUpdater {
218214
self = [super init];
219215
NSAssert(self, @"super init cannot be nil");
220-
_isInitialized = false;
221-
_isPlaying = false;
222-
_disposed = false;
223216

224217
AVAsset* asset = [item asset];
225218
void (^assetCompletionHandler)(void) = ^{
@@ -352,7 +345,7 @@ - (void)setupEventSinkIfReadyToPlay {
352345
return;
353346
}
354347

355-
_isInitialized = true;
348+
_isInitialized = YES;
356349
_eventSink(@{
357350
@"event" : @"initialized",
358351
@"duration" : @([self duration]),
@@ -363,12 +356,12 @@ - (void)setupEventSinkIfReadyToPlay {
363356
}
364357

365358
- (void)play {
366-
_isPlaying = true;
359+
_isPlaying = YES;
367360
[self updatePlayingState];
368361
}
369362

370363
- (void)pause {
371-
_isPlaying = false;
364+
_isPlaying = NO;
372365
[self updatePlayingState];
373366
}
374367

@@ -389,7 +382,7 @@ - (void)seekTo:(int)location {
389382
toleranceAfter:kCMTimeZero];
390383
}
391384

392-
- (void)setIsLooping:(bool)isLooping {
385+
- (void)setIsLooping:(BOOL)isLooping {
393386
_isLooping = isLooping;
394387
}
395388

@@ -457,22 +450,18 @@ - (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
457450
/// is useful for the case where the Engine is in the process of deconstruction
458451
/// so the channel is going to die or is already dead.
459452
- (void)disposeSansEventChannel {
460-
_disposed = true;
453+
_disposed = YES;
461454
[_displayLink invalidate];
462-
[[_player currentItem] removeObserver:self forKeyPath:@"status" context:statusContext];
463-
[[_player currentItem] removeObserver:self
464-
forKeyPath:@"loadedTimeRanges"
465-
context:timeRangeContext];
466-
[[_player currentItem] removeObserver:self
467-
forKeyPath:@"playbackLikelyToKeepUp"
468-
context:playbackLikelyToKeepUpContext];
469-
[[_player currentItem] removeObserver:self
470-
forKeyPath:@"playbackBufferEmpty"
471-
context:playbackBufferEmptyContext];
472-
[[_player currentItem] removeObserver:self
473-
forKeyPath:@"playbackBufferFull"
474-
context:playbackBufferFullContext];
475-
[_player replaceCurrentItemWithPlayerItem:nil];
455+
AVPlayerItem* currentItem = self.player.currentItem;
456+
[currentItem removeObserver:self forKeyPath:@"status"];
457+
[currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
458+
[currentItem removeObserver:self forKeyPath:@"presentationSize"];
459+
[currentItem removeObserver:self forKeyPath:@"duration"];
460+
[currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
461+
[currentItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
462+
[currentItem removeObserver:self forKeyPath:@"playbackBufferFull"];
463+
464+
[self.player replaceCurrentItemWithPlayerItem:nil];
476465
[[NSNotificationCenter defaultCenter] removeObserver:self];
477466
}
478467

@@ -486,7 +475,8 @@ - (void)dispose {
486475
@interface FLTVideoPlayerPlugin () <FLTVideoPlayerApi>
487476
@property(readonly, weak, nonatomic) NSObject<FlutterTextureRegistry>* registry;
488477
@property(readonly, weak, nonatomic) NSObject<FlutterBinaryMessenger>* messenger;
489-
@property(readonly, strong, nonatomic) NSMutableDictionary* players;
478+
@property(readonly, strong, nonatomic)
479+
NSMutableDictionary<NSNumber*, FLTVideoPlayer*>* playersByTextureId;
490480
@property(readonly, strong, nonatomic) NSObject<FlutterPluginRegistrar>* registrar;
491481
@end
492482

@@ -503,16 +493,13 @@ - (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
503493
_registry = [registrar textures];
504494
_messenger = [registrar messenger];
505495
_registrar = registrar;
506-
_players = [NSMutableDictionary dictionaryWithCapacity:1];
496+
_playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1];
507497
return self;
508498
}
509499

510500
- (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
511-
for (NSNumber* textureId in _players.allKeys) {
512-
FLTVideoPlayer* player = _players[textureId];
513-
[player disposeSansEventChannel];
514-
}
515-
[_players removeAllObjects];
501+
[self.playersByTextureId.allValues makeObjectsPerformSelector:@selector(disposeSansEventChannel)];
502+
[self.playersByTextureId removeAllObjects];
516503
// TODO(57151): This should be commented out when 57151's fix lands on stable.
517504
// This is the correct behavior we never did it in the past and the engine
518505
// doesn't currently support it.
@@ -521,15 +508,15 @@ - (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar>*)registra
521508

522509
- (FLTTextureMessage*)onPlayerSetup:(FLTVideoPlayer*)player
523510
frameUpdater:(FLTFrameUpdater*)frameUpdater {
524-
int64_t textureId = [_registry registerTexture:player];
511+
int64_t textureId = [self.registry registerTexture:player];
525512
frameUpdater.textureId = textureId;
526513
FlutterEventChannel* eventChannel = [FlutterEventChannel
527514
eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%lld",
528515
textureId]
529516
binaryMessenger:_messenger];
530517
[eventChannel setStreamHandler:player];
531518
player.eventChannel = eventChannel;
532-
_players[@(textureId)] = player;
519+
self.playersByTextureId[@(textureId)] = player;
533520
FLTTextureMessage* result = [[FLTTextureMessage alloc] init];
534521
result.textureId = @(textureId);
535522
return result;
@@ -539,11 +526,12 @@ - (void)initialize:(FlutterError* __autoreleasing*)error {
539526
// Allow audio playback when the Ring/Silent switch is set to silent
540527
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
541528

542-
for (NSNumber* textureId in _players) {
543-
[_registry unregisterTexture:[textureId unsignedIntegerValue]];
544-
[_players[textureId] dispose];
545-
}
546-
[_players removeAllObjects];
529+
[self.playersByTextureId
530+
enumerateKeysAndObjectsUsingBlock:^(NSNumber* textureId, FLTVideoPlayer* player, BOOL* stop) {
531+
[self.registry unregisterTexture:textureId.unsignedIntegerValue];
532+
[player dispose];
533+
}];
534+
[self.playersByTextureId removeAllObjects];
547535
}
548536

549537
- (FLTTextureMessage*)create:(FLTCreateMessage*)input error:(FlutterError**)error {
@@ -570,9 +558,9 @@ - (FLTTextureMessage*)create:(FLTCreateMessage*)input error:(FlutterError**)erro
570558
}
571559

572560
- (void)dispose:(FLTTextureMessage*)input error:(FlutterError**)error {
573-
FLTVideoPlayer* player = _players[input.textureId];
574-
[_registry unregisterTexture:input.textureId.intValue];
575-
[_players removeObjectForKey:input.textureId];
561+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
562+
[self.registry unregisterTexture:input.textureId.intValue];
563+
[self.playersByTextureId removeObjectForKey:input.textureId];
576564
// If the Flutter contains https://github.com/flutter/engine/pull/12695,
577565
// the `player` is disposed via `onTextureUnregistered` at the right time.
578566
// Without https://github.com/flutter/engine/pull/12695, there is no guarantee that the
@@ -592,46 +580,46 @@ - (void)dispose:(FLTTextureMessage*)input error:(FlutterError**)error {
592580
}
593581

594582
- (void)setLooping:(FLTLoopingMessage*)input error:(FlutterError**)error {
595-
FLTVideoPlayer* player = _players[input.textureId];
596-
[player setIsLooping:[input.isLooping boolValue]];
583+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
584+
player.isLooping = input.isLooping.boolValue;
597585
}
598586

599587
- (void)setVolume:(FLTVolumeMessage*)input error:(FlutterError**)error {
600-
FLTVideoPlayer* player = _players[input.textureId];
601-
[player setVolume:[input.volume doubleValue]];
588+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
589+
[player setVolume:input.volume.doubleValue];
602590
}
603591

604592
- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage*)input error:(FlutterError**)error {
605-
FLTVideoPlayer* player = _players[input.textureId];
606-
[player setPlaybackSpeed:[input.speed doubleValue]];
593+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
594+
[player setPlaybackSpeed:input.speed.doubleValue];
607595
}
608596

609597
- (void)play:(FLTTextureMessage*)input error:(FlutterError**)error {
610-
FLTVideoPlayer* player = _players[input.textureId];
598+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
611599
[player play];
612600
}
613601

614602
- (FLTPositionMessage*)position:(FLTTextureMessage*)input error:(FlutterError**)error {
615-
FLTVideoPlayer* player = _players[input.textureId];
603+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
616604
FLTPositionMessage* result = [[FLTPositionMessage alloc] init];
617605
result.position = @([player position]);
618606
return result;
619607
}
620608

621609
- (void)seekTo:(FLTPositionMessage*)input error:(FlutterError**)error {
622-
FLTVideoPlayer* player = _players[input.textureId];
623-
[player seekTo:[input.position intValue]];
624-
[_registry textureFrameAvailable:input.textureId.intValue];
610+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
611+
[player seekTo:input.position.intValue];
612+
[self.registry textureFrameAvailable:input.textureId.intValue];
625613
}
626614

627615
- (void)pause:(FLTTextureMessage*)input error:(FlutterError**)error {
628-
FLTVideoPlayer* player = _players[input.textureId];
616+
FLTVideoPlayer* player = self.playersByTextureId[input.textureId];
629617
[player pause];
630618
}
631619

632620
- (void)setMixWithOthers:(FLTMixWithOthersMessage*)input
633621
error:(FlutterError* _Nullable __autoreleasing*)error {
634-
if ([input.mixWithOthers boolValue]) {
622+
if (input.mixWithOthers.boolValue) {
635623
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
636624
withOptions:AVAudioSessionCategoryOptionMixWithOthers
637625
error:nil];

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, and web.
44
repository: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.2.13
6+
version: 2.2.14
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)