Skip to content

Commit c070b0a

Browse files
[video_player] Add macOS support (flutter#4982)
Adds macOS support to `video_player`, sharing almost all of the code with iOS. Notes about changes at a high level: - macOS does not have `CADisplayLink` (prior to 14, and even there without all the functionality we need), so this adds macOS compilation branches that use the lower-level `CVDisplayLink` instead. Per the TODO, this code should be extracted later to reduce `ifdef`s in what is already a complicated file. - Adds KVO unregistration on `dealloc` if it wasn't done in `dispose`, since unit tests were crashing on macOS with that. - Temporarily ifdef's out `publish:` for macOS, with a TODO to re-enable it after the next stable. Most of flutter/flutter#41688 Once this lands, the app-facing package will be updated to endorse it for macOS.
1 parent 79461c2 commit c070b0a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1864
-152
lines changed

packages/video_player/video_player_avfoundation/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.5.0
2+
3+
* Adds support for macOS.
4+
15
## 2.4.11
26

37
* Updates Pigeon.

packages/video_player/video_player_avfoundation/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# video\_player\_avfoundation
22

3-
The iOS implementation of [`video_player`][1].
3+
The iOS and macOS implementation of [`video_player`][1].
44

55
## Usage
66

packages/video_player/video_player_avfoundation/ios/Classes/FVPVideoPlayerPlugin.h renamed to packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.h

+4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
#if TARGET_OS_OSX
6+
#import <FlutterMacOS/FlutterMacOS.h>
7+
#else
58
#import <Flutter/Flutter.h>
9+
#endif
610

711
@interface FVPVideoPlayerPlugin : NSObject <FlutterPlugin>
812
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar;

packages/video_player/video_player_avfoundation/ios/Classes/FVPVideoPlayerPlugin.m renamed to packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m

+156-26
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
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

70112
static 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

Comments
 (0)