From 42a5ba45faca1fb1b78fb91bcd06fd9ffe0c7436 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:32:42 -0700 Subject: [PATCH 01/12] Make sure the AVPlayerItem is ready to play before sending initialized event to the plugin --- .../FVPVideoPlayer.m | 147 ++++++------------ 1 file changed, 48 insertions(+), 99 deletions(-) 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 59e82934e16..a69d6666be6 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 @@ -12,8 +12,6 @@ static void *timeRangeContext = &timeRangeContext; static void *statusContext = &statusContext; -static void *presentationSizeContext = &presentationSizeContext; -static void *durationContext = &durationContext; static void *playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext; static void *rateContext = &rateContext; @@ -126,14 +124,6 @@ - (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player { forKeyPath:@"status" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:statusContext]; - [item addObserver:self - forKeyPath:@"presentationSize" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:presentationSizeContext]; - [item addObserver:self - forKeyPath:@"duration" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:durationContext]; [item addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew @@ -158,6 +148,7 @@ - (void)itemDidPlayToEndTime:(NSNotification *)notification { AVPlayerItem *p = [notification object]; [p seekToTime:kCMTimeZero completionHandler:nil]; } else { + NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); if (_eventSink) { _eventSink(@{@"event" : @"completed"}); } @@ -227,16 +218,18 @@ - (void)observeValueForKeyPath:(NSString *)path ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); + if (!_eventSink) { + return; + } if (context == timeRangeContext) { - if (_eventSink != nil) { - NSMutableArray *> *values = [[NSMutableArray alloc] init]; - for (NSValue *rangeValue in [object loadedTimeRanges]) { - CMTimeRange range = [rangeValue CMTimeRangeValue]; - int64_t start = FVPCMTimeToMillis(range.start); - [values addObject:@[ @(start), @(start + FVPCMTimeToMillis(range.duration)) ]]; - } - _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); + NSMutableArray *> *values = [[NSMutableArray alloc] init]; + for (NSValue *rangeValue in [object loadedTimeRanges]) { + CMTimeRange range = [rangeValue CMTimeRangeValue]; + int64_t start = FVPCMTimeToMillis(range.start); + [values addObject:@[ @(start), @(start + FVPCMTimeToMillis(range.duration)) ]]; } + _eventSink(@{@"event" : @"bufferingUpdate", @"values" : values}); } else if (context == statusContext) { AVPlayerItem *item = (AVPlayerItem *)object; switch (item.status) { @@ -247,36 +240,21 @@ - (void)observeValueForKeyPath:(NSString *)path break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; - [self setupEventSinkIfReadyToPlay]; + [self sendVideoInitializedEvent]; break; } - } else if (context == presentationSizeContext || context == durationContext) { - AVPlayerItem *item = (AVPlayerItem *)object; - if (item.status == AVPlayerItemStatusReadyToPlay) { - // 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]; - } } else if (context == playbackLikelyToKeepUpContext) { [self updatePlayingState]; if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingEnd"}); - } + _eventSink(@{@"event" : @"bufferingEnd"}); } else { - if (_eventSink != nil) { - _eventSink(@{@"event" : @"bufferingStart"}); - } + _eventSink(@{@"event" : @"bufferingStart"}); } } 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}); - } + _eventSink(@{@"event" : @"isPlayingStateUpdate", @"isPlaying" : player.rate > 0 ? @YES : @NO}); } } @@ -326,9 +304,7 @@ - (void)updateRate { } - (void)sendFailedToLoadVideoEvent { - if (_eventSink == nil) { - return; - } + NSAssert(_eventSink, @"sendFailedToLoadVideoEvent was called when the event sink was nil."); // Prefer more detailed error information from tracks loading. NSError *error; if ([self.player.currentItem.asset statusOfValueForKey:@"tracks" @@ -351,58 +327,24 @@ - (void)sendFailedToLoadVideoEvent { _eventSink([FlutterError errorWithCode:@"VideoError" message:message details:nil]); } -- (void)setupEventSinkIfReadyToPlay { - if (_eventSink && !_isInitialized) { - AVPlayerItem *currentItem = self.player.currentItem; - CGSize size = currentItem.presentationSize; - CGFloat width = size.width; - CGFloat height = size.height; - - // Wait until tracks are loaded to check duration or if there are any videos. - AVAsset *asset = currentItem.asset; - if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { - void (^trackCompletionHandler)(void) = ^{ - if ([asset statusOfValueForKey:@"tracks" error:nil] != AVKeyValueStatusLoaded) { - // Cancelled, or something failed. - return; - } - // This completion block will run on an AVFoundation background queue. - // Hop back to the main thread to set up event sink. - [self performSelector:_cmd onThread:NSThread.mainThread withObject:self waitUntilDone:NO]; - }; - [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ] - completionHandler:trackCompletionHandler]; - return; - } - - BOOL hasVideoTracks = [asset tracksWithMediaType:AVMediaTypeVideo].count != 0; - // Audio-only HLS files have no size, so `currentItem.tracks.count` must be used to check for - // track presence, as AVAsset does not always provide track information in HLS streams. - BOOL hasNoTracks = currentItem.tracks.count == 0 && asset.tracks.count == 0; - - // The player has not yet initialized when it has no size, unless it is an audio-only track. - // HLS m3u8 video files never load any tracks, and are also not yet initialized until they have - // a size. - if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height && - width == CGSizeZero.width) { - return; - } - // The player may be initialized but still needs to determine the duration. - int64_t duration = [self duration]; - if (duration == 0) { - return; - } - - _isInitialized = YES; - [self updatePlayingState]; - - _eventSink(@{ - @"event" : @"initialized", - @"duration" : @(duration), - @"width" : @(width), - @"height" : @(height) - }); - } +- (void)sendVideoInitializedEvent { + AVPlayerItem *currentItem = self.player.currentItem; + NSAssert(currentItem.status == AVPlayerItemStatusReadyToPlay, + @"sendVideoInitializedEvent was called when the item wasn't ready to play."); + NSAssert(_eventSink, @"sendVideoInitializedEvent was called when the event sink was nil."); + NSAssert(!_isInitialized, @"sendVideoInitializedEvent should only be called once."); + CGSize size = currentItem.presentationSize; + CGFloat width = size.width; + CGFloat height = size.height; + + _isInitialized = YES; + [self updatePlayingState]; + _eventSink(@{ + @"event" : @"initialized", + @"duration" : @(self.duration), + @"width" : @(width), + @"height" : @(height) + }); } - (void)play { @@ -457,12 +399,15 @@ - (void)setPlaybackSpeed:(double)speed { } - (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); _eventSink = nil; + _isInitialized = NO; return nil; } - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { + NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); _eventSink = events; // TODO(@recastrodiaz): remove the line below when the race condition is resolved: // https://github.com/flutter/flutter/issues/21483 @@ -472,12 +417,18 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments // 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; + switch (self.player.currentItem.status) { + case AVPlayerItemStatusUnknown: + return nil; + case AVPlayerItemStatusReadyToPlay: + if (!_isInitialized) { + [self sendVideoInitializedEvent]; + } + return nil; + case AVPlayerItemStatusFailed: + [self sendFailedToLoadVideoEvent]; + return nil; } - [self setupEventSinkIfReadyToPlay]; - return nil; } /// This method allows you to dispose without touching the event channel. This @@ -503,8 +454,6 @@ - (void)removeKeyValueObservers { AVPlayerItem *currentItem = _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"]; [_player removeObserver:self forKeyPath:@"rate"]; } From bb89c41fff8f9fac4b088bfd04be482fe144ab5a Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 30 Jun 2025 13:43:01 -0700 Subject: [PATCH 02/12] wuh --- .../Sources/video_player_avfoundation/FVPVideoPlayer.m | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 a69d6666be6..656264d7959 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 @@ -245,11 +245,10 @@ - (void)observeValueForKeyPath:(NSString *)path } } else if (context == playbackLikelyToKeepUpContext) { [self updatePlayingState]; - if ([[_player currentItem] isPlaybackLikelyToKeepUp]) { - _eventSink(@{@"event" : @"bufferingEnd"}); - } else { - _eventSink(@{@"event" : @"bufferingStart"}); - } + NSString* event = [[_player currentItem] isPlaybackLikelyToKeepUp] + ? @"bufferingEnd" + : @"bufferingStart"; + _eventSink(@{@"event" : event}); } 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. From 1ad1d2eacc9613f54ca37cda176c834ab7a21963 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:15:55 -0700 Subject: [PATCH 03/12] docs --- .../FVPVideoPlayer.m | 26 +++++++------------ .../FVPVideoPlayer_Internal.h | 8 +++++- 2 files changed, 16 insertions(+), 18 deletions(-) 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 656264d7959..8f6e5ba4c57 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 @@ -233,21 +233,20 @@ - (void)observeValueForKeyPath:(NSString *)path } else if (context == statusContext) { AVPlayerItem *item = (AVPlayerItem *)object; switch (item.status) { - case AVPlayerItemStatusFailed: - [self sendFailedToLoadVideoEvent]; - break; case AVPlayerItemStatusUnknown: break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; [self sendVideoInitializedEvent]; break; + case AVPlayerItemStatusFailed: + [self sendFailedToLoadVideoEvent]; + break; } } else if (context == playbackLikelyToKeepUpContext) { [self updatePlayingState]; - NSString* event = [[_player currentItem] isPlaybackLikelyToKeepUp] - ? @"bufferingEnd" - : @"bufferingStart"; + NSString *event = + [[_player currentItem] isPlaybackLikelyToKeepUp] ? @"bufferingEnd" : @"bufferingStart"; _eventSink(@{@"event" : event}); } else if (context == rateContext) { // Important: Make sure to cast the object to AVPlayer when observing the rate property, @@ -408,21 +407,14 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments eventSink:(nonnull FlutterEventSink)events { NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); _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 switch (self.player.currentItem.status) { case AVPlayerItemStatusUnknown: + // Subscription happens before the AVPlayerItem becomes ready to play. + // snedVideoInitializedEvent or sendFailedToLoadVideoEvent will be + // called by the KVO method on status updates. return nil; case AVPlayerItemStatusReadyToPlay: - if (!_isInitialized) { - [self sendVideoInitializedEvent]; - } + [self sendVideoInitializedEvent]; return nil; case AVPlayerItemStatusFailed: [self sendFailedToLoadVideoEvent]; 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 7b282be4c06..9c300d46e16 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 @@ -29,7 +29,13 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) NSNumber *targetPlaybackSpeed; /// Indicates whether the video player is currently playing. @property(nonatomic, readonly) BOOL isPlaying; -/// Indicates whether the video player has been initialized. +/// Indicates whether an "initialized" message has been sent to the current Flutter event sink. +/// +/// The video player sends an "initialized" message to the event sink when its underlying +/// AVPlayerItem is ready to play and the event sink is set to a non-nil value, whichever occurs +/// last. +/// +/// This flag is set back to NO when event sink is set to nil in onCancelWithArgument. @property(nonatomic, readonly) BOOL isInitialized; /// Initializes a new instance of FVPVideoPlayer with the given AVPlayerItem, frame updater, display From 63d240b5791b2601d33231a944fae0c057d99544 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:49:38 -0700 Subject: [PATCH 04/12] example app and README.md --- .../video_player_avfoundation/CHANGELOG.md | 1 + .../FVPVideoPlayer.m | 6 ++--- .../example/lib/mini_controller.dart | 22 +++++++++---------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 935a6eabe93..3b15305056e 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -2,6 +2,7 @@ * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. * Refactors native code for improved testing. +* Removes unnecessary workarounds, fixes "initialized" event not firing when the duration of the media is 0. ## 2.7.1 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 8f6e5ba4c57..5ea673945c4 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 @@ -409,9 +409,9 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments _eventSink = events; switch (self.player.currentItem.status) { case AVPlayerItemStatusUnknown: - // Subscription happens before the AVPlayerItem becomes ready to play. - // snedVideoInitializedEvent or sendFailedToLoadVideoEvent will be - // called by the KVO method on status updates. + // When this method is called when the media is still loading, do nothing: + // sendVideoInitializedEvent or sendFailedToLoadVideoEvent will be called + // by KVO on status updates. return nil; case AVPlayerItemStatusReadyToPlay: [self sendVideoInitializedEvent]; diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 7d406ecbd1a..0ead272b21e 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math' as math show max; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -526,29 +527,28 @@ class _VideoProgressIndicatorState extends State { const Color bufferedColor = Color.fromRGBO(50, 50, 200, 0.2); const Color backgroundColor = Color.fromRGBO(200, 200, 200, 0.5); - Widget progressIndicator; + final Widget progressIndicator; if (controller.value.isInitialized) { final int duration = controller.value.duration.inMilliseconds; final int position = controller.value.position.inMilliseconds; - int maxBuffering = 0; - for (final DurationRange range in controller.value.buffered) { - final int end = range.end.inMilliseconds; - if (end > maxBuffering) { - maxBuffering = end; - } - } - + final double maxBuffering = duration == 0.0 + ? 0.0 + : controller.value.buffered + .map( + (DurationRange range) => range.end.inMilliseconds) + .fold(0, math.max) / + duration; progressIndicator = Stack( fit: StackFit.passthrough, children: [ LinearProgressIndicator( - value: maxBuffering / duration, + value: maxBuffering, valueColor: const AlwaysStoppedAnimation(bufferedColor), backgroundColor: backgroundColor, ), LinearProgressIndicator( - value: position / duration, + value: duration == 0.0 ? 0.0 : position / duration, valueColor: const AlwaysStoppedAnimation(playedColor), backgroundColor: Colors.transparent, ), From 0cfe3f4da8e1444b3325b342c1890785175c145c Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:31:13 -0700 Subject: [PATCH 05/12] format --- .../video_player_avfoundation/example/lib/mini_controller.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart index 0ead272b21e..47d6bf39b08 100644 --- a/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart +++ b/packages/video_player/video_player_avfoundation/example/lib/mini_controller.dart @@ -535,8 +535,7 @@ class _VideoProgressIndicatorState extends State { final double maxBuffering = duration == 0.0 ? 0.0 : controller.value.buffered - .map( - (DurationRange range) => range.end.inMilliseconds) + .map((DurationRange range) => range.end.inMilliseconds) .fold(0, math.max) / duration; progressIndicator = Stack( From 60fc5d28ad4f5c0e4a827bd721efa9a4c1332e14 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 7 Jul 2025 15:34:54 -0700 Subject: [PATCH 06/12] Update CHANGELOG.md --- packages/video_player/video_player_avfoundation/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 3b15305056e..a5eab734ee3 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,8 +1,8 @@ ## NEXT +* Removes unnecessary workarounds, fixes "initialized" event not firing when the duration of the media is 0. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. * Refactors native code for improved testing. -* Removes unnecessary workarounds, fixes "initialized" event not firing when the duration of the media is 0. ## 2.7.1 From bcc27251b4e717ed65b0bd96a5187841fa417f37 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:04:23 -0700 Subject: [PATCH 07/12] Update pubspec.yaml --- packages/video_player/video_player_avfoundation/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index ded01c7a74d..0385861197c 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.7.2 +version: 2.7.3 environment: sdk: ^3.6.0 From 733f5ea0f104902377de923e7504a365fe27ac25 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Mon, 7 Jul 2025 16:04:42 -0700 Subject: [PATCH 08/12] Update CHANGELOG.md --- packages/video_player/video_player_avfoundation/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index 30037dd899d..a5ae634441b 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 2.7.3 * Removes unnecessary workarounds, fixes "initialized" event not firing when the duration of the media is 0. From 35c9a08a426966fb2c76b89daac53328b52e0f66 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:09:11 -0700 Subject: [PATCH 09/12] Update pubspec.yaml --- packages/video_player/video_player_avfoundation/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 89bc263053a..1aa9c522ca5 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.7.3 +version: 2.7.4 environment: sdk: ^3.6.0 From 4315c4a9b36ef9b465c52065058ea94752d5a298 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:11:18 -0700 Subject: [PATCH 10/12] oops --- .../FVPVideoPlayer.m | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) 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 a191e41671b..a12d6474f6d 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 @@ -237,10 +237,10 @@ - (void)observeValueForKeyPath:(NSString *)path change:(NSDictionary *)change context:(void *)context { NSAssert([NSThread isMainThread], @"event sink must only be accessed from the main thread"); - if (!_eventSink) { - return; - } if (context == timeRangeContext) { + if (!_eventSink) { + return; + } NSMutableArray *> *values = [[NSMutableArray alloc] init]; for (NSValue *rangeValue in [object loadedTimeRanges]) { CMTimeRange range = [rangeValue CMTimeRangeValue]; @@ -255,18 +255,28 @@ - (void)observeValueForKeyPath:(NSString *)path break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; - [self sendVideoInitializedEvent]; + if (_eventSink) { + [self sendVideoInitializedEvent]; + } break; case AVPlayerItemStatusFailed: - [self sendFailedToLoadVideoEvent]; + if (_eventSink) { + [self sendFailedToLoadVideoEvent]; + } break; } } else if (context == playbackLikelyToKeepUpContext) { + if (!_eventSink) { + return; + } [self updatePlayingState]; NSString *event = [[_player currentItem] isPlaybackLikelyToKeepUp] ? @"bufferingEnd" : @"bufferingStart"; _eventSink(@{@"event" : event}); } else if (context == rateContext) { + if (!_eventSink) { + return; + } // 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; From e2ce9fda2aa38ab61b77007709c71209fe93ecfb Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:12:47 -0700 Subject: [PATCH 11/12] add a test --- .../darwin/RunnerTests/VideoPlayerTests.m | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) 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 0dd84c55149..3ee2a86f376 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -951,6 +951,39 @@ - (void)testPlayerShouldNotDropEverySecondFrame { OCMVerifyAllWithDelay(mockTextureRegistry, 10); } +- (void)testVideoOutputIsAddedWhenAVPlayerItemBecomesReady { + NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + FVPVideoPlayerPlugin *videoPlayerPlugin = + [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar]; + FlutterError *error; + [videoPlayerPlugin initialize:&error]; + XCTAssertNil(error); + FVPCreationOptions *create = [FVPCreationOptions + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4" + packageName:nil + formatHint:nil + httpHeaders:@{} + viewType:FVPPlatformVideoViewTypeTextureView]; + + NSNumber *playerIdentifier = [videoPlayerPlugin createWithOptions:create error:&error]; + FVPVideoPlayer *player = videoPlayerPlugin.playersByIdentifier[playerIdentifier]; + XCTAssertNotNil(player); + + AVPlayerItem *item = player.player.currentItem; + [self keyValueObservingExpectationForObject:(id)item + keyPath:@"status" + expectedValue:@(AVPlayerItemStatusReadyToPlay)]; + [self waitForExpectationsWithTimeout:10.0 handler:nil]; + // Video output is added as soon as the status becomes ready to play. + XCTAssertEqual(item.outputs.count, 1); + + [player onListenWithArguments:nil + eventSink:^(FlutterError *event){ + }]; + XCTAssertEqual(item.outputs.count, 1); +} + #if TARGET_OS_IOS - (void)testVideoPlayerShouldNotOverwritePlayAndRecordNorDefaultToSpeaker { NSObject *registrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); From 6f232f553015ca2f2c11bf144b4c6deef218e3a0 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:35:02 -0700 Subject: [PATCH 12/12] gemini review --- .../Sources/video_player_avfoundation/FVPVideoPlayer.m | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 a12d6474f6d..683e8a5ea12 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 @@ -254,7 +254,9 @@ - (void)observeValueForKeyPath:(NSString *)path case AVPlayerItemStatusUnknown: break; case AVPlayerItemStatusReadyToPlay: - [item addOutput:_videoOutput]; + if (![item.outputs containsObject:_videoOutput]) { + [item addOutput:_videoOutput]; + } if (_eventSink) { [self sendVideoInitializedEvent]; } @@ -446,6 +448,9 @@ - (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments case AVPlayerItemStatusFailed: [self sendFailedToLoadVideoEvent]; return nil; + default: + NSAssert(NO, @"Unknown AVPlayerItemStatus: %ld", (long)self.player.currentItem.status); + return nil; } }