18
18
@interface FVPFrameUpdater : NSObject
19
19
@property (nonatomic ) int64_t textureId;
20
20
@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
21
24
- (void )onDisplayLink : (CADisplayLink *)link ;
25
+ #endif
22
26
@end
23
27
24
28
@implementation FVPFrameUpdater
@@ -29,11 +33,34 @@ - (FVPFrameUpdater *)initWithRegistry:(NSObject<FlutterTextureRegistry> *)regist
29
33
return self;
30
34
}
31
35
36
+ #if TARGET_OS_IOS
32
37
- (void )onDisplayLink : (CADisplayLink *)link {
38
+ // TODO(stuartmorgan): Investigate switching this to displayLinkFired; iOS may also benefit from
39
+ // the availability check there.
33
40
[_registry textureFrameAvailable: _textureId];
34
41
}
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
+ }
35
51
@end
36
52
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
+
37
64
@interface FVPDefaultPlayerFactory : NSObject <FVPPlayerFactory>
38
65
@end
39
66
@@ -53,18 +80,33 @@ @interface FVPVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
53
80
// An invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
54
81
// for issue #1, and restore the correct width and height for issue #2.
55
82
@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;
57
87
@property (nonatomic ) FlutterEventChannel *eventChannel;
58
88
@property (nonatomic ) FlutterEventSink eventSink;
59
89
@property (nonatomic ) CGAffineTransform preferredTransform;
60
90
@property (nonatomic , readonly ) BOOL disposed;
61
91
@property (nonatomic , readonly ) BOOL isPlaying;
62
92
@property (nonatomic ) BOOL isLooping;
63
93
@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
+
64
105
- (instancetype )initWithURL : (NSURL *)url
65
106
frameUpdater : (FVPFrameUpdater *)frameUpdater
66
107
httpHeaders : (nonnull NSDictionary <NSString *, NSString *> *)headers
67
- playerFactory : (id <FVPPlayerFactory>)playerFactory ;
108
+ playerFactory : (id <FVPPlayerFactory>)playerFactory
109
+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar ;
68
110
@end
69
111
70
112
static void *timeRangeContext = &timeRangeContext;
@@ -77,12 +119,27 @@ - (instancetype)initWithURL:(NSURL *)url
77
119
@implementation FVPVideoPlayer
78
120
- (instancetype )initWithAsset : (NSString *)asset
79
121
frameUpdater : (FVPFrameUpdater *)frameUpdater
80
- playerFactory : (id <FVPPlayerFactory>)playerFactory {
122
+ playerFactory : (id <FVPPlayerFactory>)playerFactory
123
+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
81
124
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
82
132
return [self initWithURL: [NSURL fileURLWithPath: path]
83
133
frameUpdater: frameUpdater
84
134
httpHeaders: @{}
85
- playerFactory: playerFactory];
135
+ playerFactory: playerFactory
136
+ registrar: registrar];
137
+ }
138
+
139
+ - (void )dealloc {
140
+ if (!_disposed) {
141
+ [self removeKeyValueObservers ];
142
+ }
86
143
}
87
144
88
145
- (void )addObserversForItem : (AVPlayerItem *)item player : (AVPlayer *)player {
@@ -153,15 +210,6 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
153
210
return degrees;
154
211
};
155
212
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
-
165
213
- (AVMutableVideoComposition *)getVideoCompositionWithTransform : (CGAffineTransform )transform
166
214
withAsset : (AVAsset *)asset
167
215
withVideoTrack : (AVAssetTrack *)videoTrack {
@@ -202,31 +250,55 @@ - (void)createVideoOutputAndDisplayLink:(FVPFrameUpdater *)frameUpdater {
202
250
};
203
251
_videoOutput = [[AVPlayerItemVideoOutput alloc ] initWithPixelBufferAttributes: pixBuffAttributes];
204
252
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
205
269
_displayLink = [CADisplayLink displayLinkWithTarget: frameUpdater
206
270
selector: @selector (onDisplayLink: )];
207
271
[_displayLink addToRunLoop: [NSRunLoop currentRunLoop ] forMode: NSRunLoopCommonModes ];
208
272
_displayLink.paused = YES ;
273
+ #endif
209
274
}
210
275
211
276
- (instancetype )initWithURL : (NSURL *)url
212
277
frameUpdater : (FVPFrameUpdater *)frameUpdater
213
278
httpHeaders : (nonnull NSDictionary <NSString *, NSString *> *)headers
214
- playerFactory : (id <FVPPlayerFactory>)playerFactory {
279
+ playerFactory : (id <FVPPlayerFactory>)playerFactory
280
+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
215
281
NSDictionary <NSString *, id > *options = nil ;
216
282
if ([headers count ] != 0 ) {
217
283
options = @{@" AVURLAssetHTTPHeaderFieldsKey" : headers};
218
284
}
219
285
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL: url options: options];
220
286
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];
222
291
}
223
292
224
293
- (instancetype )initWithPlayerItem : (AVPlayerItem *)item
225
294
frameUpdater : (FVPFrameUpdater *)frameUpdater
226
- playerFactory : (id <FVPPlayerFactory>)playerFactory {
295
+ playerFactory : (id <FVPPlayerFactory>)playerFactory
296
+ registrar : (NSObject <FlutterPluginRegistrar> *)registrar {
227
297
self = [super init ];
228
298
NSAssert (self, @" super init cannot be nil" );
229
299
300
+ _registrar = registrar;
301
+
230
302
AVAsset *asset = [item asset ];
231
303
void (^assetCompletionHandler)(void ) = ^{
232
304
if ([asset statusOfValueForKey: @" tracks" error: nil ] == AVKeyValueStatusLoaded) {
@@ -265,7 +337,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
265
337
// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
266
338
// for issue #1, and restore the correct width and height for issue #2.
267
339
_playerLayer = [AVPlayerLayer playerLayerWithPlayer: _player];
268
- [rootViewController ().view.layer addSublayer: _playerLayer];
340
+ [self .flutterViewLayer addSublayer: _playerLayer];
269
341
270
342
[self createVideoOutputAndDisplayLink: frameUpdater];
271
343
@@ -350,7 +422,23 @@ - (void)updatePlayingState {
350
422
} else {
351
423
[_player pause ];
352
424
}
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
353
440
_displayLink.paused = !_isPlaying;
441
+ #endif
354
442
}
355
443
356
444
- (void )setupEventSinkIfReadyToPlay {
@@ -515,14 +603,17 @@ - (void)disposeSansEventChannel {
515
603
516
604
_disposed = YES ;
517
605
[_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
518
614
[_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 ];
526
617
527
618
[self .player replaceCurrentItemWithPlayerItem: nil ];
528
619
[[NSNotificationCenter defaultCenter ] removeObserver: self ];
@@ -533,6 +624,33 @@ - (void)dispose {
533
624
[_eventChannel setStreamHandler: nil ];
534
625
}
535
626
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
+
536
654
@end
537
655
538
656
@interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
@@ -547,7 +665,11 @@ @interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
547
665
@implementation FVPVideoPlayerPlugin
548
666
+ (void )registerWithRegistrar : (NSObject <FlutterPluginRegistrar> *)registrar {
549
667
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
550
671
[registrar publish: instance];
672
+ #endif
551
673
FVPAVFoundationVideoPlayerApiSetup (registrar.messenger , instance);
552
674
}
553
675
@@ -592,8 +714,10 @@ - (FVPTextureMessage *)onPlayerSetup:(FVPVideoPlayer *)player
592
714
}
593
715
594
716
- (void )initialize : (FlutterError *__autoreleasing *)error {
717
+ #if TARGET_OS_IOS
595
718
// Allow audio playback when the Ring/Silent switch is set to silent
596
719
[[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback error: nil ];
720
+ #endif
597
721
598
722
[self .playersByTextureId
599
723
enumerateKeysAndObjectsUsingBlock: ^(NSNumber *textureId, FVPVideoPlayer *player, BOOL *stop) {
@@ -616,7 +740,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
616
740
@try {
617
741
player = [[FVPVideoPlayer alloc ] initWithAsset: assetPath
618
742
frameUpdater: frameUpdater
619
- playerFactory: _playerFactory];
743
+ playerFactory: _playerFactory
744
+ registrar: self .registrar];
620
745
return [self onPlayerSetup: player frameUpdater: frameUpdater];
621
746
} @catch (NSException *exception ) {
622
747
*error = [FlutterError errorWithCode: @" video_player" message: exception .reason details: nil ];
@@ -626,7 +751,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
626
751
player = [[FVPVideoPlayer alloc ] initWithURL: [NSURL URLWithString: input.uri]
627
752
frameUpdater: frameUpdater
628
753
httpHeaders: input.httpHeaders
629
- playerFactory: _playerFactory];
754
+ playerFactory: _playerFactory
755
+ registrar: self .registrar];
630
756
return [self onPlayerSetup: player frameUpdater: frameUpdater];
631
757
} else {
632
758
*error = [FlutterError errorWithCode: @" video_player" message: @" not implemented" details: nil ];
@@ -702,13 +828,17 @@ - (void)pause:(FVPTextureMessage *)input error:(FlutterError **)error {
702
828
703
829
- (void )setMixWithOthers : (FVPMixWithOthersMessage *)input
704
830
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
705
834
if (input.mixWithOthers .boolValue ) {
706
835
[[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback
707
836
withOptions: AVAudioSessionCategoryOptionMixWithOthers
708
837
error: nil ];
709
838
} else {
710
839
[[AVAudioSession sharedInstance ] setCategory: AVAudioSessionCategoryPlayback error: nil ];
711
840
}
841
+ #endif
712
842
}
713
843
714
844
@end
0 commit comments