1818@interface FVPFrameUpdater : NSObject
1919@property (nonatomic ) int64_t textureId;
2020@property (nonatomic , weak , readonly ) NSObject <FlutterTextureRegistry> *registry;
21+ // The output that this updater is managing.
22+ @property (nonatomic , weak ) AVPlayerItemVideoOutput *videoOutput;
23+ #if TARGET_OS_IOS
2124- (void )onDisplayLink : (CADisplayLink *)link ;
25+ #endif
2226@end
2327
2428@implementation FVPFrameUpdater
@@ -29,11 +33,34 @@ - (FVPFrameUpdater *)initWithRegistry:(NSObject<FlutterTextureRegistry> *)regist
2933 return self;
3034}
3135
36+ #if TARGET_OS_IOS
3237- (void )onDisplayLink : (CADisplayLink *)link {
38+ // TODO(stuartmorgan): Investigate switching this to displayLinkFired; iOS may also benefit from
39+ // the availability check there.
3340 [_registry textureFrameAvailable: _textureId];
3441}
42+ #endif
43+
44+ - (void )displayLinkFired {
45+ // Only report a new frame if one is actually available.
46+ CMTime outputItemTime = [self .videoOutput itemTimeForHostTime: CACurrentMediaTime ()];
47+ if ([self .videoOutput hasNewPixelBufferForItemTime: outputItemTime]) {
48+ [_registry textureFrameAvailable: _textureId];
49+ }
50+ }
3551@end
3652
53+ #if TARGET_OS_OSX
54+ static CVReturn DisplayLinkCallback (CVDisplayLinkRef displayLink, const CVTimeStamp *now,
55+ const CVTimeStamp *outputTime, CVOptionFlags flagsIn,
56+ CVOptionFlags *flagsOut, void *displayLinkSource) {
57+ // Trigger the main-thread dispatch queue, to drive a frame update check.
58+ __weak dispatch_source_t source = (__bridge dispatch_source_t )displayLinkSource;
59+ dispatch_source_merge_data (source, 1 );
60+ return kCVReturnSuccess ;
61+ }
62+ #endif
63+
3764@interface FVPDefaultPlayerFactory : NSObject <FVPPlayerFactory>
3865@end
3966
@@ -53,18 +80,33 @@ @interface FVPVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
5380// An invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
5481// for issue #1, and restore the correct width and height for issue #2.
5582@property (readonly , nonatomic ) AVPlayerLayer *playerLayer;
56- @property (readonly , nonatomic ) CADisplayLink *displayLink;
83+ // The plugin registrar, to obtain view information from.
84+ @property (nonatomic , weak ) NSObject <FlutterPluginRegistrar> *registrar;
85+ // The CALayer associated with the Flutter view this plugin is associated with, if any.
86+ @property (nonatomic , readonly ) CALayer *flutterViewLayer;
5787@property (nonatomic ) FlutterEventChannel *eventChannel;
5888@property (nonatomic ) FlutterEventSink eventSink;
5989@property (nonatomic ) CGAffineTransform preferredTransform;
6090@property (nonatomic , readonly ) BOOL disposed;
6191@property (nonatomic , readonly ) BOOL isPlaying;
6292@property (nonatomic ) BOOL isLooping;
6393@property (nonatomic , readonly ) BOOL isInitialized;
94+ // TODO(stuartmorgan): Extract and abstract the display link to remove all the display-link-related
95+ // ifdefs from this file.
96+ #if TARGET_OS_OSX
97+ // The display link to trigger frame reads from the video player.
98+ @property (nonatomic , assign ) CVDisplayLinkRef displayLink;
99+ // A dispatch source to move display link callbacks to the main thread.
100+ @property (nonatomic , strong ) dispatch_source_t displayLinkSource;
101+ #else
102+ @property (nonatomic ) CADisplayLink *displayLink;
103+ #endif
104+
64105- (instancetype )initWithURL : (NSURL *)url
65106 frameUpdater : (FVPFrameUpdater *)frameUpdater
66107 httpHeaders : (nonnull NSDictionary <NSString *, NSString *> *)headers
67- playerFactory : (id <FVPPlayerFactory>)playerFactory ;
108+ playerFactory : (id <FVPPlayerFactory>)playerFactory
109+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar ;
68110@end
69111
70112static void *timeRangeContext = &timeRangeContext;
@@ -77,12 +119,27 @@ - (instancetype)initWithURL:(NSURL *)url
77119@implementation FVPVideoPlayer
78120- (instancetype )initWithAsset : (NSString *)asset
79121 frameUpdater : (FVPFrameUpdater *)frameUpdater
80- playerFactory : (id <FVPPlayerFactory>)playerFactory {
122+ playerFactory : (id <FVPPlayerFactory>)playerFactory
123+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
81124 NSString *path = [[NSBundle mainBundle ] pathForResource: asset ofType: nil ];
125+ #if TARGET_OS_OSX
126+ // See https://github.com/flutter/flutter/issues/135302
127+ // TODO(stuartmorgan): Remove this if the asset APIs are adjusted to work better for macOS.
128+ if (!path) {
129+ path = [NSURL URLWithString: asset relativeToURL: NSBundle .mainBundle.bundleURL].path ;
130+ }
131+ #endif
82132 return [self initWithURL: [NSURL fileURLWithPath: path]
83133 frameUpdater: frameUpdater
84134 httpHeaders: @{}
85- playerFactory: playerFactory];
135+ playerFactory: playerFactory
136+ registrar: registrar];
137+ }
138+
139+ - (void )dealloc {
140+ if (!_disposed) {
141+ [self removeKeyValueObservers ];
142+ }
86143}
87144
88145- (void )addObserversForItem : (AVPlayerItem *)item player : (AVPlayer *)player {
@@ -153,15 +210,6 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
153210 return degrees;
154211};
155212
156- NS_INLINE UIViewController *rootViewController (void ) {
157- #pragma clang diagnostic push
158- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
159- // TODO: (hellohuanlin) Provide a non-deprecated codepath. See
160- // https://github.com/flutter/flutter/issues/104117
161- return UIApplication.sharedApplication .keyWindow .rootViewController ;
162- #pragma clang diagnostic pop
163- }
164-
165213- (AVMutableVideoComposition *)getVideoCompositionWithTransform : (CGAffineTransform)transform
166214 withAsset : (AVAsset *)asset
167215 withVideoTrack : (AVAssetTrack *)videoTrack {
@@ -202,31 +250,55 @@ - (void)createVideoOutputAndDisplayLink:(FVPFrameUpdater *)frameUpdater {
202250 };
203251 _videoOutput = [[AVPlayerItemVideoOutput alloc ] initWithPixelBufferAttributes: pixBuffAttributes];
204252
253+ #if TARGET_OS_OSX
254+ frameUpdater.videoOutput = _videoOutput;
255+ // Create and start the main-thread dispatch queue to drive frameUpdater.
256+ self.displayLinkSource =
257+ dispatch_source_create (DISPATCH_SOURCE_TYPE_DATA_ADD, 0 , 0 , dispatch_get_main_queue ());
258+ dispatch_source_set_event_handler (self.displayLinkSource , ^() {
259+ @autoreleasepool {
260+ [frameUpdater displayLinkFired ];
261+ }
262+ });
263+ dispatch_resume (self.displayLinkSource );
264+ if (CVDisplayLinkCreateWithActiveCGDisplays (&_displayLink) == kCVReturnSuccess ) {
265+ CVDisplayLinkSetOutputCallback (_displayLink, &DisplayLinkCallback,
266+ (__bridge void *)(self.displayLinkSource ));
267+ }
268+ #else
205269 _displayLink = [CADisplayLink displayLinkWithTarget: frameUpdater
206270 selector: @selector (onDisplayLink: )];
207271 [_displayLink addToRunLoop: [NSRunLoop currentRunLoop ] forMode: NSRunLoopCommonModes ];
208272 _displayLink.paused = YES ;
273+ #endif
209274}
210275
211276- (instancetype )initWithURL : (NSURL *)url
212277 frameUpdater : (FVPFrameUpdater *)frameUpdater
213278 httpHeaders : (nonnull NSDictionary <NSString *, NSString *> *)headers
214- playerFactory : (id <FVPPlayerFactory>)playerFactory {
279+ playerFactory : (id <FVPPlayerFactory>)playerFactory
280+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
215281 NSDictionary <NSString *, id > *options = nil ;
216282 if ([headers count ] != 0 ) {
217283 options = @{@" AVURLAssetHTTPHeaderFieldsKey" : headers};
218284 }
219285 AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL: url options: options];
220286 AVPlayerItem *item = [AVPlayerItem playerItemWithAsset: urlAsset];
221- return [self initWithPlayerItem: item frameUpdater: frameUpdater playerFactory: playerFactory];
287+ return [self initWithPlayerItem: item
288+ frameUpdater: frameUpdater
289+ playerFactory: playerFactory
290+ registrar: registrar];
222291}
223292
224293- (instancetype )initWithPlayerItem : (AVPlayerItem *)item
225294 frameUpdater : (FVPFrameUpdater *)frameUpdater
226- playerFactory : (id <FVPPlayerFactory>)playerFactory {
295+ playerFactory : (id <FVPPlayerFactory>)playerFactory
296+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
227297 self = [super init ];
228298 NSAssert (self, @" super init cannot be nil" );
229299
300+ _registrar = registrar;
301+
230302 AVAsset *asset = [item asset ];
231303 void (^assetCompletionHandler)(void ) = ^{
232304 if ([asset statusOfValueForKey: @" tracks" error: nil ] == AVKeyValueStatusLoaded) {
@@ -265,7 +337,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
265337 // invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
266338 // for issue #1, and restore the correct width and height for issue #2.
267339 _playerLayer = [AVPlayerLayer playerLayerWithPlayer: _player];
268- [rootViewController ().view.layer addSublayer: _playerLayer];
340+ [self .flutterViewLayer addSublayer: _playerLayer];
269341
270342 [self createVideoOutputAndDisplayLink: frameUpdater];
271343
@@ -350,7 +422,23 @@ - (void)updatePlayingState {
350422 } else {
351423 [_player pause ];
352424 }
425+ #if TARGET_OS_OSX
426+ if (_displayLink) {
427+ if (_isPlaying) {
428+ NSScreen *screen = self.registrar .view .window .screen ;
429+ if (screen) {
430+ CGDirectDisplayID viewDisplayID =
431+ (CGDirectDisplayID)[screen.deviceDescription[@" NSScreenNumber" ] unsignedIntegerValue ];
432+ CVDisplayLinkSetCurrentCGDisplay (_displayLink, viewDisplayID);
433+ }
434+ CVDisplayLinkStart (_displayLink);
435+ } else {
436+ CVDisplayLinkStop (_displayLink);
437+ }
438+ }
439+ #else
353440 _displayLink.paused = !_isPlaying;
441+ #endif
354442}
355443
356444- (void )setupEventSinkIfReadyToPlay {
@@ -515,14 +603,17 @@ - (void)disposeSansEventChannel {
515603
516604 _disposed = YES ;
517605 [_playerLayer removeFromSuperlayer ];
606+ #if TARGET_OS_OSX
607+ if (_displayLink) {
608+ CVDisplayLinkStop (_displayLink);
609+ CVDisplayLinkRelease (_displayLink);
610+ _displayLink = NULL ;
611+ }
612+ dispatch_source_cancel (_displayLinkSource);
613+ #else
518614 [_displayLink invalidate ];
519- AVPlayerItem *currentItem = self.player .currentItem ;
520- [currentItem removeObserver: self forKeyPath: @" status" ];
521- [currentItem removeObserver: self forKeyPath: @" loadedTimeRanges" ];
522- [currentItem removeObserver: self forKeyPath: @" presentationSize" ];
523- [currentItem removeObserver: self forKeyPath: @" duration" ];
524- [currentItem removeObserver: self forKeyPath: @" playbackLikelyToKeepUp" ];
525- [self .player removeObserver: self forKeyPath: @" rate" ];
615+ #endif
616+ [self removeKeyValueObservers ];
526617
527618 [self .player replaceCurrentItemWithPlayerItem: nil ];
528619 [[NSNotificationCenter defaultCenter ] removeObserver: self ];
@@ -533,6 +624,33 @@ - (void)dispose {
533624 [_eventChannel setStreamHandler: nil ];
534625}
535626
627+ - (CALayer *)flutterViewLayer {
628+ #if TARGET_OS_OSX
629+ return self.registrar .view .layer ;
630+ #else
631+ #pragma clang diagnostic push
632+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
633+ // TODO(hellohuanlin): Provide a non-deprecated codepath. See
634+ // https://github.com/flutter/flutter/issues/104117
635+ UIViewController *root = UIApplication.sharedApplication .keyWindow .rootViewController ;
636+ #pragma clang diagnostic pop
637+ return root.view .layer ;
638+ #endif
639+ }
640+
641+ // / Removes all key-value observers set up for the player.
642+ // /
643+ // / This is called from dealloc, so must not use any methods on self.
644+ - (void )removeKeyValueObservers {
645+ AVPlayerItem *currentItem = _player.currentItem ;
646+ [currentItem removeObserver: self forKeyPath: @" status" ];
647+ [currentItem removeObserver: self forKeyPath: @" loadedTimeRanges" ];
648+ [currentItem removeObserver: self forKeyPath: @" presentationSize" ];
649+ [currentItem removeObserver: self forKeyPath: @" duration" ];
650+ [currentItem removeObserver: self forKeyPath: @" playbackLikelyToKeepUp" ];
651+ [_player removeObserver: self forKeyPath: @" rate" ];
652+ }
653+
536654@end
537655
538656@interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
@@ -547,7 +665,11 @@ @interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
547665@implementation FVPVideoPlayerPlugin
548666+ (void )registerWithRegistrar : (NSObject <FlutterPluginRegistrar> *)registrar {
549667 FVPVideoPlayerPlugin *instance = [[FVPVideoPlayerPlugin alloc ] initWithRegistrar: registrar];
668+ #if !TARGET_OS_OSX
669+ // TODO(stuartmorgan): Remove the ifdef once >3.13 reaches stable. See
670+ // https://github.com/flutter/flutter/issues/135320
550671 [registrar publish: instance];
672+ #endif
551673 FVPAVFoundationVideoPlayerApiSetup (registrar.messenger , instance);
552674}
553675
@@ -592,8 +714,10 @@ - (FVPTextureMessage *)onPlayerSetup:(FVPVideoPlayer *)player
592714}
593715
594716- (void )initialize : (FlutterError *__autoreleasing *)error {
717+ #if TARGET_OS_IOS
595718 // Allow audio playback when the Ring/Silent switch is set to silent
596719 [[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback error: nil ];
720+ #endif
597721
598722 [self .playersByTextureId
599723 enumerateKeysAndObjectsUsingBlock: ^(NSNumber *textureId, FVPVideoPlayer *player, BOOL *stop) {
@@ -616,7 +740,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
616740 @try {
617741 player = [[FVPVideoPlayer alloc ] initWithAsset: assetPath
618742 frameUpdater: frameUpdater
619- playerFactory: _playerFactory];
743+ playerFactory: _playerFactory
744+ registrar: self .registrar];
620745 return [self onPlayerSetup: player frameUpdater: frameUpdater];
621746 } @catch (NSException *exception) {
622747 *error = [FlutterError errorWithCode: @" video_player" message: exception.reason details: nil ];
@@ -626,7 +751,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
626751 player = [[FVPVideoPlayer alloc ] initWithURL: [NSURL URLWithString: input.uri]
627752 frameUpdater: frameUpdater
628753 httpHeaders: input.httpHeaders
629- playerFactory: _playerFactory];
754+ playerFactory: _playerFactory
755+ registrar: self .registrar];
630756 return [self onPlayerSetup: player frameUpdater: frameUpdater];
631757 } else {
632758 *error = [FlutterError errorWithCode: @" video_player" message: @" not implemented" details: nil ];
@@ -702,13 +828,17 @@ - (void)pause:(FVPTextureMessage *)input error:(FlutterError **)error {
702828
703829- (void )setMixWithOthers : (FVPMixWithOthersMessage *)input
704830 error : (FlutterError *_Nullable __autoreleasing *)error {
831+ #if TARGET_OS_OSX
832+ // AVAudioSession doesn't exist on macOS, and audio always mixes, so just no-op.
833+ #else
705834 if (input.mixWithOthers .boolValue ) {
706835 [[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback
707836 withOptions: AVAudioSessionCategoryOptionMixWithOthers
708837 error: nil ];
709838 } else {
710839 [[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback error: nil ];
711840 }
841+ #endif
712842}
713843
714844@end
0 commit comments