diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index e98f438609f..e2afaf91d44 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.2 + +* Restructure internals of Dart notification of video player events. + ## 2.8.1 * Restructures internal logic to move more code to Dart. diff --git a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m index 58ad53674d0..9541226aeff 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -8,6 +8,7 @@ #import #import +#import #import #import #import @@ -166,6 +167,55 @@ - (instancetype)init { #pragma mark - +@interface StubEventListener : NSObject + +@property(nonatomic) XCTestExpectation *initializationExpectation; +@property(nonatomic) int64_t initializationDuration; +@property(nonatomic) CGSize initializationSize; + +- (instancetype)initWithInitializationExpectation:(XCTestExpectation *)expectation; + +@end + +@implementation StubEventListener + +- (instancetype)initWithInitializationExpectation:(XCTestExpectation *)expectation { + self = [super init]; + _initializationExpectation = expectation; + return self; +} + +- (void)videoPlayerDidComplete { +} + +- (void)videoPlayerDidEndBuffering { +} + +- (void)videoPlayerDidErrorWithMessage:(NSString *)errorMessage { +} + +- (void)videoPlayerDidInitializeWithDuration:(int64_t)duration size:(CGSize)size { + [self.initializationExpectation fulfill]; + self.initializationDuration = duration; + self.initializationSize = size; +} + +- (void)videoPlayerDidSetPlaying:(BOOL)playing { +} + +- (void)videoPlayerDidStartBuffering { +} + +- (void)videoPlayerDidUpdateBufferRegions:(NSArray *> *)regions { +} + +- (void)videoPlayerWasDisposed { +} + +@end + +#pragma mark - + @implementation VideoPlayerTests - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { @@ -220,9 +270,9 @@ - (void)testPlayerForPlatformViewDoesNotRegisterTexture { viewProvider:[[StubViewProvider alloc] initWithView:nil] registrar:registrar]; - FlutterError *initalizationError; - [videoPlayerPlugin initialize:&initalizationError]; - XCTAssertNil(initalizationError); + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" httpHeaders:@{} @@ -248,9 +298,9 @@ - (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { viewProvider:[[StubViewProvider alloc] initWithView:nil] registrar:registrar]; - FlutterError *initalizationError; - [videoPlayerPlugin initialize:&initalizationError]; - XCTAssertNil(initalizationError); + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" httpHeaders:@{} @@ -305,9 +355,9 @@ - (void)testInitStartsDisplayLinkTemporarily { viewProvider:[[StubViewProvider alloc] initWithView:nil] registrar:registrar]; - FlutterError *initalizationError; - [videoPlayerPlugin initialize:&initalizationError]; - XCTAssertNil(initalizationError); + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" httpHeaders:@{} @@ -351,9 +401,9 @@ - (void)testSeekToWhilePlayingDoesNotStopDisplayLink { viewProvider:[[StubViewProvider alloc] initWithView:nil] registrar:registrar]; - FlutterError *initalizationError; - [videoPlayerPlugin initialize:&initalizationError]; - XCTAssertNil(initalizationError); + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" httpHeaders:@{} @@ -406,9 +456,9 @@ - (void)testPauseWhileWaitingForFrameDoesNotStopDisplayLink { viewProvider:[[StubViewProvider alloc] initWithView:nil] registrar:registrar]; - FlutterError *initalizationError; - [videoPlayerPlugin initialize:&initalizationError]; - XCTAssertNil(initalizationError); + FlutterError *initializationError; + [videoPlayerPlugin initialize:&initializationError]; + XCTAssertNil(initializationError); FVPCreationOptions *create = [FVPCreationOptions makeWithUri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" httpHeaders:@{} @@ -477,16 +527,19 @@ - (void)testBufferingStateFromPlayer { AVPlayer *avPlayer = player.player; [avPlayer play]; - [player onListenWithArguments:nil - eventSink:^(NSDictionary *event) { - if ([event[@"event"] isEqualToString:@"bufferingEnd"]) { - XCTAssertTrue(avPlayer.currentItem.isPlaybackLikelyToKeepUp); - } - - if ([event[@"event"] isEqualToString:@"bufferingStart"]) { - XCTAssertFalse(avPlayer.currentItem.isPlaybackLikelyToKeepUp); - } - }]; + // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit + // tests of FVPEventBridge. + [(NSObject *)player.eventListener + onListenWithArguments:nil + eventSink:^(NSDictionary *event) { + if ([event[@"event"] isEqualToString:@"bufferingEnd"]) { + XCTAssertTrue(avPlayer.currentItem.isPlaybackLikelyToKeepUp); + } + + if ([event[@"event"] isEqualToString:@"bufferingStart"]) { + XCTAssertFalse(avPlayer.currentItem.isPlaybackLikelyToKeepUp); + } + }]; XCTestExpectation *bufferingStateExpectation = [self expectationWithDescription:@"bufferingState"]; NSTimeInterval timeout = 10; @@ -498,39 +551,39 @@ - (void)testBufferingStateFromPlayer { } - (void)testVideoControls { - NSDictionary *videoInitialization = + StubEventListener *eventListener = [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4"]; - XCTAssertEqualObjects(videoInitialization[@"height"], @720); - XCTAssertEqualObjects(videoInitialization[@"width"], @1280); - XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); + XCTAssertEqual(eventListener.initializationSize.height, 720); + XCTAssertEqual(eventListener.initializationSize.width, 1280); + XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); } - (void)testAudioControls { - NSDictionary *audioInitialization = [self + StubEventListener *eventListener = [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/audio/rooster.mp3"]; - XCTAssertEqualObjects(audioInitialization[@"height"], @0); - XCTAssertEqualObjects(audioInitialization[@"width"], @0); + XCTAssertEqual(eventListener.initializationSize.height, 0); + XCTAssertEqual(eventListener.initializationSize.width, 0); // Perfect precision not guaranteed. - XCTAssertEqualWithAccuracy([audioInitialization[@"duration"] intValue], 5400, 200); + XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 5400, 200); } - (void)testHLSControls { - NSDictionary *videoInitialization = [self + StubEventListener *eventListener = [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8"]; - XCTAssertEqualObjects(videoInitialization[@"height"], @720); - XCTAssertEqualObjects(videoInitialization[@"width"], @1280); - XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); + XCTAssertEqual(eventListener.initializationSize.height, 720); + XCTAssertEqual(eventListener.initializationSize.width, 1280); + XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); } - (void)testAudioOnlyHLSControls { XCTSkip(@"Flaky; see https://github.com/flutter/flutter/issues/164381"); - NSDictionary *videoInitialization = + StubEventListener *eventListener = [self sanityTestURI:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/" @"bee_audio_only.m3u8"]; - XCTAssertEqualObjects(videoInitialization[@"height"], @0); - XCTAssertEqualObjects(videoInitialization[@"width"], @0); - XCTAssertEqualWithAccuracy([videoInitialization[@"duration"] intValue], 4000, 200); + XCTAssertEqual(eventListener.initializationSize.height, 0); + XCTAssertEqual(eventListener.initializationSize.width, 0); + XCTAssertEqualWithAccuracy(eventListener.initializationDuration, 4000, 200); } #if TARGET_OS_IOS @@ -555,6 +608,8 @@ - (void)testSeekToleranceWhenNotSeekingToEnd { httpHeaders:@{} avFactory:stubAVFactory viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + NSObject *listener = OCMProtocolMock(@protocol(FVPVideoEventListener)); + player.eventListener = listener; XCTestExpectation *seekExpectation = [self expectationWithDescription:@"seekTo has zero tolerance when seeking not to end"]; @@ -577,6 +632,8 @@ - (void)testSeekToleranceWhenSeekingToEnd { httpHeaders:@{} avFactory:stubAVFactory viewProvider:[[StubViewProvider alloc] initWithView:nil]]; + NSObject *listener = OCMProtocolMock(@protocol(FVPVideoEventListener)); + player.eventListener = listener; XCTestExpectation *seekExpectation = [self expectationWithDescription:@"seekTo has non-zero tolerance when seeking to end"]; @@ -592,7 +649,9 @@ - (void)testSeekToleranceWhenSeekingToEnd { /// Sanity checks a video player playing the given URL with the actual AVPlayer. This is essentially /// a mini integration test of the player component. -- (NSDictionary *)sanityTestURI:(NSString *)testURI { +/// +/// Returns the stub event listener to allow tests to inspect the call state. +- (StubEventListener *)sanityTestURI:(NSString *)testURI { NSURL *testURL = [NSURL URLWithString:testURI]; XCTAssertNotNil(testURL); FVPVideoPlayer *player = @@ -603,15 +662,9 @@ - (void)testSeekToleranceWhenSeekingToEnd { XCTAssertNotNil(player); XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - __block NSDictionary *initializationEvent; - [player onListenWithArguments:nil - eventSink:^(NSDictionary *event) { - if ([event[@"event"] isEqualToString:@"initialized"]) { - initializationEvent = event; - XCTAssertEqual(event.count, 4); - [initializedExpectation fulfill]; - } - }]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; [self waitForExpectationsWithTimeout:30.0 handler:nil]; // Starts paused. @@ -634,9 +687,7 @@ - (void)testSeekToleranceWhenSeekingToEnd { XCTAssertNil(error); XCTAssertEqual(avPlayer.volume, 0.1f); - [player onCancelWithArguments:nil]; - - return initializationEvent; + return listener; } // Checks whether [AVPlayer rate] KVO observations are correctly detached. @@ -787,12 +838,15 @@ - (void)testFailedToLoadVideoEventShouldBeAlwaysSent { [self waitForExpectationsWithTimeout:10.0 handler:nil]; XCTestExpectation *failedExpectation = [self expectationWithDescription:@"failed"]; - [player onListenWithArguments:nil - eventSink:^(FlutterError *event) { - if ([event isKindOfClass:FlutterError.class]) { - [failedExpectation fulfill]; - } - }]; + // TODO(stuartmorgan): Update this test to instead use a mock listener, and add separate unit + // tests of FVPEventBridge. + [(NSObject *)player.eventListener + onListenWithArguments:nil + eventSink:^(FlutterError *event) { + if ([event isKindOfClass:FlutterError.class]) { + [failedExpectation fulfill]; + } + }]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; } @@ -804,12 +858,9 @@ - (void)testUpdatePlayingStateShouldNotResetRate { viewProvider:[[StubViewProvider alloc] initWithView:nil]]; XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"initialized"]; - [player onListenWithArguments:nil - eventSink:^(NSDictionary *event) { - if ([event[@"event"] isEqualToString:@"initialized"]) { - [initializedExpectation fulfill]; - } - }]; + StubEventListener *listener = + [[StubEventListener alloc] initWithInitializationExpectation:initializedExpectation]; + player.eventListener = listener; [self waitForExpectationsWithTimeout:10 handler:nil]; FlutterError *error; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m new file mode 100644 index 00000000000..55550a885e7 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPEventBridge.m @@ -0,0 +1,122 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "./include/video_player_avfoundation/FVPEventBridge.h" + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +@interface FVPEventBridge () + +/// The event channel to dispatch notifications to. +// TODO(stuartmorgan): Convert this to Pigeon event channels once the plugin is using Swift +// Pigeon generation. +@property(nonatomic) FlutterEventChannel *eventChannel; + +/// The event sink associated with eventChannel. +/// +/// Will be nil both before the channel listener is ready on the Dart side, and after it has been +/// closed on the Dart side. +@property(nonatomic, nullable) FlutterEventSink eventSink; + +/// A queue of events received before eventSink is ready, to dispatch once the channel is fully +/// set up. +@property(nonatomic) NSMutableArray *queuedEvents; + +@end + +@implementation FVPEventBridge + +- (instancetype)initWithMessenger:(NSObject *)messenger + channelName:(NSString *)channelName { + self = [super init]; + if (self) { + _queuedEvents = [[NSMutableArray alloc] init]; + _eventChannel = [FlutterEventChannel eventChannelWithName:channelName + binaryMessenger:messenger]; + // This retain loop is broken in videoPlayerWasDisposed. + [_eventChannel setStreamHandler:self]; + } + return self; +} + +#pragma mark FlutterStreamHandler + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + self.eventSink = events; + + // Send any events that came in before the sink was ready. + for (id event in self.queuedEvents) { + self.eventSink(event); + } + [self.queuedEvents removeAllObjects]; + + return nil; +} + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + self.eventSink = nil; + // No need to queue events coming in after this point; nil the queue so they will be discarded. + self.queuedEvents = nil; + return nil; +} + +#pragma mark FVPVideoEventListener + +- (void)videoPlayerDidInitializeWithDuration:(int64_t)duration size:(CGSize)size { + [self sendOrQueue:@{ + @"event" : @"initialized", + @"duration" : @(duration), + @"width" : @(size.width), + @"height" : @(size.height) + }]; +} + +- (void)videoPlayerDidErrorWithMessage:(NSString *)errorMessage { + [self sendOrQueue:[FlutterError errorWithCode:@"VideoError" message:errorMessage details:nil]]; +} + +- (void)videoPlayerDidComplete { + [self sendOrQueue:@{@"event" : @"completed"}]; +} + +- (void)videoPlayerDidStartBuffering { + [self sendOrQueue:@{@"event" : @"bufferingStart"}]; +} + +- (void)videoPlayerDidEndBuffering { + [self sendOrQueue:@{@"event" : @"bufferingEnd"}]; +} + +- (void)videoPlayerDidUpdateBufferRegions:(NSArray *> *)regions { + [self sendOrQueue:@{@"event" : @"bufferingUpdate", @"values" : regions}]; +} + +- (void)videoPlayerDidSetPlaying:(BOOL)playing { + [self sendOrQueue:@{@"event" : @"isPlayingStateUpdate", @"isPlaying" : @(playing)}]; +} + +- (void)videoPlayerWasDisposed { + [self.eventChannel setStreamHandler:nil]; +} + +#pragma mark Private methods + +/// Sends the given event to the event sink if it is ready to receive events, or enqueues it to send +/// later if not. +- (void)sendOrQueue:(id)event { + if (self.eventSink) { + self.eventSink(event); + } else { + [self.queuedEvents addObject:event]; + } +} + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m index d8ef40c456e..425fd6f7af8 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPTextureBasedVideoPlayer.m @@ -118,7 +118,7 @@ - (void)seekTo:(NSInteger)position completion:(void (^)(FlutterError *_Nullable) }]; } -- (void)disposeSansEventChannel { +- (void)dispose { // 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 [FVPVideoPlayerPlugin initialize:] method and then disposed again by @@ -127,7 +127,7 @@ - (void)disposeSansEventChannel { return; } - [super disposeSansEventChannel]; + [super dispose]; [self.playerLayer removeFromSuperlayer]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m index acf5bf60805..4db671b8ab9 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayer.m @@ -95,23 +95,17 @@ - (void)dealloc { } } -/// This method allows you to dispose without touching the event channel. This -/// 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 { +- (void)dispose { _disposed = YES; [self removeKeyValueObservers]; [self.player replaceCurrentItemWithPlayerItem:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self]; -} -- (void)dispose { - [self disposeSansEventChannel]; if (_onDisposed) { _onDisposed(); } - [_eventChannel setStreamHandler:nil]; + [self.eventListener videoPlayerWasDisposed]; } - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { @@ -155,9 +149,7 @@ - (void)itemDidPlayToEndTime:(NSNotification *)notification { AVPlayerItem *p = [notification object]; [p seekToTime:kCMTimeZero completionHandler:nil]; } else { - if (_eventSink) { - _eventSink(@{@"event" : @"completed"}); - } + [self.eventListener videoPlayerDidComplete]; } } @@ -225,17 +217,15 @@ - (void)observeValueForKeyPath:(NSString *)path change:(NSDictionary *)change context:(void *)context { if (context == timeRangeContext) { - if (_eventSink != nil) { - NSMutableArray *> *values = [[NSMutableArray alloc] init]; - for (NSValue *rangeValue in [object loadedTimeRanges]) { - CMTimeRange range = [rangeValue CMTimeRangeValue]; - [values addObject:@[ - @(FVPCMTimeToMillis(range.start)), - @(FVPCMTimeToMillis(range.duration)), - ]]; - } - _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); + NSMutableArray *> *values = [[NSMutableArray alloc] init]; + for (NSValue *rangeValue in [object loadedTimeRanges]) { + CMTimeRange range = [rangeValue CMTimeRangeValue]; + [values addObject:@[ + @(FVPCMTimeToMillis(range.start)), + @(FVPCMTimeToMillis(range.duration)), + ]]; } + [self.eventListener videoPlayerDidUpdateBufferRegions:values]; } else if (context == statusContext) { AVPlayerItem *item = (AVPlayerItem *)object; switch (item.status) { @@ -246,7 +236,7 @@ - (void)observeValueForKeyPath:(NSString *)path break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; - [self setupEventSinkIfReadyToPlay]; + [self reportInitializedIfReadyToPlay]; break; } } else if (context == presentationSizeContext || context == durationContext) { @@ -255,27 +245,20 @@ - (void)observeValueForKeyPath:(NSString *)path // Due to an apparent bug, when the player item is ready, it still may not have determined // its presentation size or duration. When these properties are finally set, re-check if // all required properties and instantiate the event sink if it is not already set up. - [self setupEventSinkIfReadyToPlay]; + [self reportInitializedIfReadyToPlay]; } } else if (context == playbackLikelyToKeepUpContext) { [self updatePlayingState]; if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingEnd"}); - } + [self.eventListener videoPlayerDidEndBuffering]; } else { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingStart"}); - } + [self.eventListener videoPlayerDidStartBuffering]; } } else if (context == rateContext) { // Important: Make sure to cast the object to AVPlayer when observing the rate property, // as it is not available in AVPlayerItem. AVPlayer *player = (AVPlayer *)object; - if (_eventSink != nil) { - _eventSink( - @{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO}); - } + [self.eventListener videoPlayerDidSetPlaying:(player.rate > 0)]; } } @@ -325,9 +308,6 @@ - (void)updateRate { } - (void)sendFailedToLoadVideoEvent { - if (_eventSink == nil) { - return; - } // Prefer more detailed error information from tracks loading. NSError *error; if ([self.player.currentItem.asset statusOfValueForKey:@"tracks" @@ -347,11 +327,11 @@ - (void)sendFailedToLoadVideoEvent { add(underlyingError.localizedDescription); add(underlyingError.localizedFailureReason); NSString *message = [details.array componentsJoinedByString:@": "]; - _eventSink([FlutterError errorWithCode:@"VideoError" message:message details:nil]); + [self.eventListener videoPlayerDidErrorWithMessage:message]; } -- (void)setupEventSinkIfReadyToPlay { - if (_eventSink && !_isInitialized) { +- (void)reportInitializedIfReadyToPlay { + if (!_isInitialized) { AVPlayerItem *currentItem = self.player.currentItem; CGSize size = currentItem.presentationSize; CGFloat width = size.width; @@ -395,12 +375,7 @@ - (void)setupEventSinkIfReadyToPlay { _isInitialized = YES; [self updatePlayingState]; - _eventSink(@{ - @"event" : @"initialized", - @"duration" : @(duration), - @"width" : @(width), - @"height" : @(height) - }); + [self.eventListener videoPlayerDidInitializeWithDuration:duration size:size]; } } @@ -452,32 +427,6 @@ - (void)setPlaybackSpeed:(double)speed error:(FlutterError *_Nullable *_Nonnull) [self updatePlayingState]; } -#pragma mark - FlutterStreamHandler - -- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { - _eventSink = nil; - return nil; -} - -- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - _eventSink = events; - // TODO(@recastrodiaz): remove the line below when the race condition is resolved: - // https://github.com/flutter/flutter/issues/21483 - // This line ensures the 'initialized' event is sent when the event - // 'AVPlayerItemStatusReadyToPlay' fires before _eventSink is set (this function - // onListenWithArguments is called) - // and also send error in similar case with 'AVPlayerItemStatusFailed' - // https://github.com/flutter/flutter/issues/151475 - // https://github.com/flutter/flutter/issues/147707 - if (self.player.currentItem.status == AVPlayerItemStatusFailed) { - [self sendFailedToLoadVideoEvent]; - return nil; - } - [self setupEventSinkIfReadyToPlay]; - return nil; -} - #pragma mark - Private - (int64_t)duration { diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m index 4c53923cd42..aa9d6f74b2b 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/FVPVideoPlayerPlugin.m @@ -9,6 +9,7 @@ #import "./include/video_player_avfoundation/FVPAVFactory.h" #import "./include/video_player_avfoundation/FVPDisplayLink.h" +#import "./include/video_player_avfoundation/FVPEventBridge.h" #import "./include/video_player_avfoundation/FVPFrameUpdater.h" #import "./include/video_player_avfoundation/FVPNativeVideoViewFactory.h" #import "./include/video_player_avfoundation/FVPTextureBasedVideoPlayer.h" @@ -90,8 +91,13 @@ - (instancetype)initWithAVFactory:(id)avFactory } - (void)detachFromEngineForRegistrar:(NSObject *)registrar { - [self.playersByIdentifier.allValues - makeObjectsPerformSelector:@selector(disposeSansEventChannel)]; + for (FVPVideoPlayer *player in self.playersByIdentifier.allValues) { + // Remove the channel and texture cleanup, and the event listener, to ensure that the player + // doesn't message the engine that is no longer connected. + player.onDisposed = nil; + player.eventListener = nil; + [player dispose]; + } [self.playersByIdentifier removeAllObjects]; SetUpFVPAVFoundationVideoPlayerApi(registrar.messenger, nil); } @@ -123,12 +129,11 @@ - (int64_t)onPlayerSetup:(FVPVideoPlayer *)player { } }; // Set up the event channel. - FlutterEventChannel *eventChannel = [FlutterEventChannel - eventChannelWithName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%@", - channelSuffix] - binaryMessenger:messenger]; - [eventChannel setStreamHandler:player]; - player.eventChannel = eventChannel; + FVPEventBridge *eventBridge = [[FVPEventBridge alloc] + initWithMessenger:messenger + channelName:[NSString stringWithFormat:@"flutter.io/videoPlayer/videoEvents%@", + channelSuffix]]; + player.eventListener = eventBridge; self.playersByIdentifier[@(playerIdentifier)] = player; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPEventBridge.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPEventBridge.h new file mode 100644 index 00000000000..11f50099c3c --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPEventBridge.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "FVPVideoEventListener.h" + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +/// An implementation of FVPVideoEventListener that forwards messages to Dart via an event channel. +@interface FVPEventBridge : NSObject + +/// Initializes the the bridge to use an event channel with the given name. +- (instancetype)initWithMessenger:(NSObject *)messenger + channelName:(NSString *)channelName; + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h new file mode 100644 index 00000000000..bbeffd82c70 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoEventListener.h @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +/// Handles event/status callbacks from FVPVideoPlayer. +/// +/// This is an abstraction around the event channel to avoid coupling FVPVideoPlayer directly to +/// implementation details specific to the plugin communication method. +@protocol FVPVideoEventListener +@required +// Called when the video player has initialized. +- (void)videoPlayerDidInitializeWithDuration:(int64_t)duration size:(CGSize)size; +// Called if there is an error in video load or playback. +- (void)videoPlayerDidErrorWithMessage:(NSString *)errorMessage; +/// Called when the video player plays to the end and then stops (i.e., looping is not enabled). +- (void)videoPlayerDidComplete; +/// Called when the video player needs to buffer more in order to play witohut stopping. +- (void)videoPlayerDidStartBuffering; +/// Called when the video player has buffered enough to likely be able to play witohut stopping. +- (void)videoPlayerDidEndBuffering; +/// Called when the buffered regions change. +/// +/// The array elements are two-element arrays, each containing the start and duration, in +/// milliseconds, of a buffered region. +- (void)videoPlayerDidUpdateBufferRegions:(NSArray *> *)regions; +/// Called when the player starts or stops playing. +- (void)videoPlayerDidSetPlaying:(BOOL)playing; +/// Called when the video player has been disposed on the Dart side. +- (void)videoPlayerWasDisposed; +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h index 469dda24a61..52705c1c9b5 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer.h @@ -6,6 +6,7 @@ #import "./messages.g.h" #import "FVPAVFactory.h" +#import "FVPVideoEventListener.h" #import "FVPViewProvider.h" #if TARGET_OS_OSX @@ -21,9 +22,7 @@ NS_ASSUME_NONNULL_BEGIN /// This class contains all functionalities needed to manage video playback in platform views and is /// typically used alongside FVPNativeVideoViewFactory. If you need to display a video using a /// texture, use FVPTextureBasedVideoPlayer instead. -@interface FVPVideoPlayer : NSObject -/// The Flutter event channel used to communicate with the Flutter engine. -@property(nonatomic) FlutterEventChannel *eventChannel; +@interface FVPVideoPlayer : NSObject /// The AVPlayer instance used for video playback. @property(nonatomic, readonly) AVPlayer *player; /// Indicates whether the video player has been disposed. @@ -32,6 +31,8 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic) BOOL isLooping; /// The current playback position of the video, in milliseconds. @property(nonatomic, readonly) int64_t position; +/// The event listener to report video events to. +@property(nonatomic, nullable) NSObject *eventListener; /// A block that will be called when dispose is called. @property(nonatomic, nullable, copy) void (^onDisposed)(void); @@ -45,11 +46,6 @@ NS_ASSUME_NONNULL_BEGIN /// Disposes the video player and releases any resources it holds. - (void)dispose; -/// Disposes the video player without touching the event channel. This -/// 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; - @end NS_ASSUME_NONNULL_END diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h index c03736e62f5..dedfce6ef63 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation/Sources/video_player_avfoundation/include/video_player_avfoundation/FVPVideoPlayer_Internal.h @@ -4,15 +4,10 @@ #import #import "FVPAVFactory.h" +#import "FVPVideoEventListener.h" #import "FVPVideoPlayer.h" #import "FVPViewProvider.h" -#if TARGET_OS_OSX -#import -#else -#import -#endif - NS_ASSUME_NONNULL_BEGIN /// Interface intended for use by subclasses, but not other callers. @@ -21,8 +16,6 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) AVPlayerItemVideoOutput *videoOutput; /// The view provider, to obtain view information from. @property(nonatomic, readonly, nullable) NSObject *viewProvider; -/// The Flutter event sink used to send events to the Flutter engine. -@property(nonatomic) FlutterEventSink eventSink; /// The preferred transform for the video. It can be used to handle the rotation of the video. @property(nonatomic) CGAffineTransform preferredTransform; /// The target playback speed requested by the plugin client. diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 29e56655cac..c2e2ece35e4 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_avfoundation description: iOS and macOS 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.8.1 +version: 2.8.2 environment: sdk: ^3.6.0