Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[video_player] Add macOS support #4982

Merged
merged 30 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
38e5047
flutter create output, unmodified
stuartmorgan Sep 21, 2023
d56fbd9
Add licenses
stuartmorgan Sep 21, 2023
b6c6e4c
Add macOS metadata
stuartmorgan Sep 21, 2023
d93570f
Move source to shared location
stuartmorgan Sep 21, 2023
f6aee72
Automatic build changes
stuartmorgan Sep 21, 2023
d49c0f5
Update Pigeon to pick up macOS Obj-C support
stuartmorgan Sep 21, 2023
10f3496
Shared native tests
stuartmorgan Sep 21, 2023
d784b63
Initial compilation fixes
stuartmorgan Sep 21, 2023
3223b94
Conditionalize UIViewController usage
stuartmorgan Sep 21, 2023
e510c37
Initial pass at CVDisplayLink
stuartmorgan Sep 21, 2023
dcb3f33
Network entitlement for example
stuartmorgan Sep 21, 2023
5de83d6
Add workaround for asset loading issue
stuartmorgan Sep 22, 2023
194ed77
Missing display link bits
stuartmorgan Sep 22, 2023
66a1c50
Add display link comments and don't change iOS here
stuartmorgan Sep 22, 2023
1022796
Fix unit tests
stuartmorgan Sep 22, 2023
4806a6d
Unwind XCUITests
stuartmorgan Sep 22, 2023
797fa8d
Add macOS support
stuartmorgan Sep 22, 2023
1d8ebd4
Remove symlinks; they aren't needed for supported versions
stuartmorgan Sep 22, 2023
7a84011
Add support for stable
stuartmorgan Sep 22, 2023
22246f7
Require 3.13 due to lookupKeyForAsset
stuartmorgan Sep 22, 2023
bc8953d
Disable a warning that's flagging intentional test behavior
stuartmorgan Sep 22, 2023
30b5011
Merge branch 'main' into video-player-macos
stuartmorgan Sep 26, 2023
1007020
Add a flag to clear Flutter caches
stuartmorgan Sep 26, 2023
04b0ff9
Add the flag
stuartmorgan Sep 26, 2023
09d60e4
Merge branch 'main' into video-player-macos
stuartmorgan Sep 26, 2023
144aeca
Revert "Add the flag"
stuartmorgan Sep 27, 2023
2bd1448
Revert "Add a flag to clear Flutter caches"
stuartmorgan Sep 27, 2023
18da78b
Temporarily add verbose to linting
stuartmorgan Sep 27, 2023
6a45c4d
Reorder properties
stuartmorgan Sep 27, 2023
51f4f8f
Revert "Temporarily add verbose to linting"
stuartmorgan Sep 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.5.0

* Adds support for macOS.

## 2.4.11

* Updates Pigeon.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# video\_player\_avfoundation

The iOS implementation of [`video_player`][1].
The iOS and macOS implementation of [`video_player`][1].

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#if TARGET_OS_OSX
#import <FlutterMacOS/FlutterMacOS.h>
#else
#import <Flutter/Flutter.h>
#endif

@interface FVPVideoPlayerPlugin : NSObject <FlutterPlugin>
- (instancetype)initWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
@interface FVPFrameUpdater : NSObject
@property(nonatomic) int64_t textureId;
@property(nonatomic, weak, readonly) NSObject<FlutterTextureRegistry> *registry;
// The output that this updater is managing.
@property(nonatomic, weak) AVPlayerItemVideoOutput *videoOutput;
#if TARGET_OS_IOS
- (void)onDisplayLink:(CADisplayLink *)link;
#endif
@end

@implementation FVPFrameUpdater
Expand All @@ -29,11 +33,34 @@ - (FVPFrameUpdater *)initWithRegistry:(NSObject<FlutterTextureRegistry> *)regist
return self;
}

#if TARGET_OS_IOS
- (void)onDisplayLink:(CADisplayLink *)link {
// TODO(stuartmorgan): Investigate switching this to displayLinkFired; iOS may also benefit from
// the availability check there.
[_registry textureFrameAvailable:_textureId];
}
#endif

- (void)displayLinkFired {
// Only report a new frame if one is actually available.
CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()];
if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
[_registry textureFrameAvailable:_textureId];
}
}
@end

#if TARGET_OS_OSX
static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now,
const CVTimeStamp *outputTime, CVOptionFlags flagsIn,
CVOptionFlags *flagsOut, void *displayLinkSource) {
// Trigger the main-thread dispatch queue, to drive a frame update check.
__weak dispatch_source_t source = (__bridge dispatch_source_t)displayLinkSource;
dispatch_source_merge_data(source, 1);
return kCVReturnSuccess;
}
#endif

@interface FVPDefaultPlayerFactory : NSObject <FVPPlayerFactory>
@end

Expand All @@ -53,18 +80,33 @@ @interface FVPVideoPlayer : NSObject <FlutterTexture, FlutterStreamHandler>
// An invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
// for issue #1, and restore the correct width and height for issue #2.
@property(readonly, nonatomic) AVPlayerLayer *playerLayer;
stuartmorgan marked this conversation as resolved.
Show resolved Hide resolved
@property(readonly, nonatomic) CADisplayLink *displayLink;
// The plugin registrar, to obtain view information from.
@property(nonatomic, weak) NSObject<FlutterPluginRegistrar> *registrar;
// The CALayer associated with the Flutter view this plugin is associated with, if any.
@property(nonatomic, readonly) CALayer *flutterViewLayer;
@property(nonatomic) FlutterEventChannel *eventChannel;
@property(nonatomic) FlutterEventSink eventSink;
@property(nonatomic) CGAffineTransform preferredTransform;
@property(nonatomic, readonly) BOOL disposed;
@property(nonatomic, readonly) BOOL isPlaying;
@property(nonatomic) BOOL isLooping;
@property(nonatomic, readonly) BOOL isInitialized;
// TODO(stuartmorgan): Extract and abstract the display link to remove all the display-link-related
// ifdefs from this file.
#if TARGET_OS_OSX
// The display link to trigger frame reads from the video player.
@property(nonatomic, assign) CVDisplayLinkRef displayLink;
// A dispatch source to move display link callbacks to the main thread.
@property(nonatomic, strong) dispatch_source_t displayLinkSource;
#else
@property(nonatomic) CADisplayLink *displayLink;
#endif

- (instancetype)initWithURL:(NSURL *)url
frameUpdater:(FVPFrameUpdater *)frameUpdater
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
playerFactory:(id<FVPPlayerFactory>)playerFactory;
playerFactory:(id<FVPPlayerFactory>)playerFactory
registrar:(NSObject<FlutterPluginRegistrar> *)registrar;
@end

static void *timeRangeContext = &timeRangeContext;
Expand All @@ -77,12 +119,27 @@ - (instancetype)initWithURL:(NSURL *)url
@implementation FVPVideoPlayer
- (instancetype)initWithAsset:(NSString *)asset
frameUpdater:(FVPFrameUpdater *)frameUpdater
playerFactory:(id<FVPPlayerFactory>)playerFactory {
playerFactory:(id<FVPPlayerFactory>)playerFactory
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil];
#if TARGET_OS_OSX
// See https://github.com/flutter/flutter/issues/135302
// TODO(stuartmorgan): Remove this if the asset APIs are adjusted to work better for macOS.
if (!path) {
path = [NSURL URLWithString:asset relativeToURL:NSBundle.mainBundle.bundleURL].path;
}
#endif
return [self initWithURL:[NSURL fileURLWithPath:path]
frameUpdater:frameUpdater
httpHeaders:@{}
playerFactory:playerFactory];
playerFactory:playerFactory
registrar:registrar];
}

- (void)dealloc {
if (!_disposed) {
[self removeKeyValueObservers];
}
}

- (void)addObserversForItem:(AVPlayerItem *)item player:(AVPlayer *)player {
Expand Down Expand Up @@ -153,15 +210,6 @@ NS_INLINE CGFloat radiansToDegrees(CGFloat radians) {
return degrees;
};

NS_INLINE UIViewController *rootViewController(void) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// TODO: (hellohuanlin) Provide a non-deprecated codepath. See
// https://github.com/flutter/flutter/issues/104117
return UIApplication.sharedApplication.keyWindow.rootViewController;
#pragma clang diagnostic pop
}

- (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransform)transform
withAsset:(AVAsset *)asset
withVideoTrack:(AVAssetTrack *)videoTrack {
Expand Down Expand Up @@ -202,31 +250,55 @@ - (void)createVideoOutputAndDisplayLink:(FVPFrameUpdater *)frameUpdater {
};
_videoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pixBuffAttributes];

#if TARGET_OS_OSX
frameUpdater.videoOutput = _videoOutput;
// Create and start the main-thread dispatch queue to drive frameUpdater.
self.displayLinkSource =
dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(self.displayLinkSource, ^() {
@autoreleasepool {
[frameUpdater displayLinkFired];
}
});
dispatch_resume(self.displayLinkSource);
if (CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink) == kCVReturnSuccess) {
CVDisplayLinkSetOutputCallback(_displayLink, &DisplayLinkCallback,
(__bridge void *)(self.displayLinkSource));
}
#else
_displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater
selector:@selector(onDisplayLink:)];
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
_displayLink.paused = YES;
#endif
}

- (instancetype)initWithURL:(NSURL *)url
frameUpdater:(FVPFrameUpdater *)frameUpdater
httpHeaders:(nonnull NSDictionary<NSString *, NSString *> *)headers
playerFactory:(id<FVPPlayerFactory>)playerFactory {
playerFactory:(id<FVPPlayerFactory>)playerFactory
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
NSDictionary<NSString *, id> *options = nil;
if ([headers count] != 0) {
options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
}
AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
return [self initWithPlayerItem:item frameUpdater:frameUpdater playerFactory:playerFactory];
return [self initWithPlayerItem:item
frameUpdater:frameUpdater
playerFactory:playerFactory
registrar:registrar];
}

- (instancetype)initWithPlayerItem:(AVPlayerItem *)item
frameUpdater:(FVPFrameUpdater *)frameUpdater
playerFactory:(id<FVPPlayerFactory>)playerFactory {
playerFactory:(id<FVPPlayerFactory>)playerFactory
registrar:(NSObject<FlutterPluginRegistrar> *)registrar {
self = [super init];
NSAssert(self, @"super init cannot be nil");

_registrar = registrar;

AVAsset *asset = [item asset];
void (^assetCompletionHandler)(void) = ^{
if ([asset statusOfValueForKey:@"tracks" error:nil] == AVKeyValueStatusLoaded) {
Expand Down Expand Up @@ -265,7 +337,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item
// invisible AVPlayerLayer is used to overwrite the protection of pixel buffers in those streams
// for issue #1, and restore the correct width and height for issue #2.
_playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
[rootViewController().view.layer addSublayer:_playerLayer];
[self.flutterViewLayer addSublayer:_playerLayer];

[self createVideoOutputAndDisplayLink:frameUpdater];

Expand Down Expand Up @@ -350,7 +422,23 @@ - (void)updatePlayingState {
} else {
[_player pause];
}
#if TARGET_OS_OSX
if (_displayLink) {
if (_isPlaying) {
NSScreen *screen = self.registrar.view.window.screen;
if (screen) {
CGDirectDisplayID viewDisplayID =
(CGDirectDisplayID)[screen.deviceDescription[@"NSScreenNumber"] unsignedIntegerValue];
CVDisplayLinkSetCurrentCGDisplay(_displayLink, viewDisplayID);
}
CVDisplayLinkStart(_displayLink);
} else {
CVDisplayLinkStop(_displayLink);
}
}
#else
_displayLink.paused = !_isPlaying;
#endif
}

- (void)setupEventSinkIfReadyToPlay {
Expand Down Expand Up @@ -515,14 +603,17 @@ - (void)disposeSansEventChannel {

_disposed = YES;
[_playerLayer removeFromSuperlayer];
#if TARGET_OS_OSX
if (_displayLink) {
CVDisplayLinkStop(_displayLink);
CVDisplayLinkRelease(_displayLink);
_displayLink = NULL;
}
dispatch_source_cancel(_displayLinkSource);
#else
[_displayLink invalidate];
AVPlayerItem *currentItem = self.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"];
[self.player removeObserver:self forKeyPath:@"rate"];
#endif
[self removeKeyValueObservers];

[self.player replaceCurrentItemWithPlayerItem:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self];
Expand All @@ -533,6 +624,33 @@ - (void)dispose {
[_eventChannel setStreamHandler:nil];
}

- (CALayer *)flutterViewLayer {
#if TARGET_OS_OSX
return self.registrar.view.layer;
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// TODO(hellohuanlin): Provide a non-deprecated codepath. See
// https://github.com/flutter/flutter/issues/104117
UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;
#pragma clang diagnostic pop
return root.view.layer;
#endif
}

/// Removes all key-value observers set up for the player.
///
/// This is called from dealloc, so must not use any methods on self.
- (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"];
}

@end

@interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
Expand All @@ -547,7 +665,11 @@ @interface FVPVideoPlayerPlugin () <FVPAVFoundationVideoPlayerApi>
@implementation FVPVideoPlayerPlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
FVPVideoPlayerPlugin *instance = [[FVPVideoPlayerPlugin alloc] initWithRegistrar:registrar];
#if !TARGET_OS_OSX
// TODO(stuartmorgan): Remove the ifdef once >3.13 reaches stable. See
// https://github.com/flutter/flutter/issues/135320
[registrar publish:instance];
#endif
FVPAVFoundationVideoPlayerApiSetup(registrar.messenger, instance);
}

Expand Down Expand Up @@ -592,8 +714,10 @@ - (FVPTextureMessage *)onPlayerSetup:(FVPVideoPlayer *)player
}

- (void)initialize:(FlutterError *__autoreleasing *)error {
#if TARGET_OS_IOS
// Allow audio playback when the Ring/Silent switch is set to silent
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
#endif

[self.playersByTextureId
enumerateKeysAndObjectsUsingBlock:^(NSNumber *textureId, FVPVideoPlayer *player, BOOL *stop) {
Expand All @@ -616,7 +740,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
@try {
player = [[FVPVideoPlayer alloc] initWithAsset:assetPath
frameUpdater:frameUpdater
playerFactory:_playerFactory];
playerFactory:_playerFactory
registrar:self.registrar];
return [self onPlayerSetup:player frameUpdater:frameUpdater];
} @catch (NSException *exception) {
*error = [FlutterError errorWithCode:@"video_player" message:exception.reason details:nil];
Expand All @@ -626,7 +751,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e
player = [[FVPVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri]
frameUpdater:frameUpdater
httpHeaders:input.httpHeaders
playerFactory:_playerFactory];
playerFactory:_playerFactory
registrar:self.registrar];
return [self onPlayerSetup:player frameUpdater:frameUpdater];
} else {
*error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil];
Expand Down Expand Up @@ -702,13 +828,17 @@ - (void)pause:(FVPTextureMessage *)input error:(FlutterError **)error {

- (void)setMixWithOthers:(FVPMixWithOthersMessage *)input
error:(FlutterError *_Nullable __autoreleasing *)error {
#if TARGET_OS_OSX
// AVAudioSession doesn't exist on macOS, and audio always mixes, so just no-op.
#else
if (input.mixWithOthers.boolValue) {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:nil];
} else {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
}
#endif
}

@end
Loading