diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index efa0f9658f9..e9c2bccc159 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.5.2 + +* Fixes flickering and seek-while-paused on macOS. + ## 2.5.1 * Updates to Pigeon 13. diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPDisplayLink.h b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPDisplayLink.h new file mode 100644 index 00000000000..67c0bf75f3f --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPDisplayLink.h @@ -0,0 +1,34 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#if TARGET_OS_OSX +#import +#else +#import +#endif + +// A cross-platform display link abstraction. +@interface FVPDisplayLink : NSObject + +/** + * Whether the display link is currently running (i.e., firing events). + * + * Defaults to NO. + */ +@property(nonatomic, assign) BOOL running; + +/** + * Initializes a display link that calls the given callback when fired. + * + * The display link starts paused, so must be started, by setting 'running' to YES, before the + * callback will fire. + */ +- (instancetype)initWithRegistrar:(id)registrar + callback:(void (^)(void))callback NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m index dfee0319982..bece3f3ca9a 100644 --- a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m @@ -9,6 +9,7 @@ #import #import "AVAssetTrackUtils.h" +#import "FVPDisplayLink.h" #import "messages.g.h" #if !__has_feature(objc_arc) @@ -20,9 +21,15 @@ @interface FVPFrameUpdater : NSObject @property(nonatomic, weak, readonly) NSObject *registry; // The output that this updater is managing. @property(nonatomic, weak) AVPlayerItemVideoOutput *videoOutput; -#if TARGET_OS_IOS -- (void)onDisplayLink:(CADisplayLink *)link; -#endif +// The last time that has been validated as avaliable according to hasNewPixelBufferForItemTime:. +@property(nonatomic, assign) CMTime lastKnownAvailableTime; +// If YES, the engine is informed that a new texture is available any time the display link +// callback is fired, regardless of the videoOutput state. +// +// TODO(stuartmorgan): Investigate removing this; it exists only to preserve existing iOS behavior +// while implementing macOS, but iOS should very likely be doing the check as well. See +// https://github.com/flutter/flutter/issues/138427. +@property(nonatomic, assign) BOOL skipBufferAvailabilityCheck; @end @implementation FVPFrameUpdater @@ -30,56 +37,57 @@ - (FVPFrameUpdater *)initWithRegistry:(NSObject *)regist NSAssert(self, @"super init cannot be nil"); if (self == nil) return nil; _registry = registry; + _lastKnownAvailableTime = kCMTimeInvalid; 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]) { + // Only report a new frame if one is actually available, or the check is being skipped. + BOOL reportFrame = NO; + if (self.skipBufferAvailabilityCheck) { + reportFrame = YES; + } else { + CMTime outputItemTime = [self.videoOutput itemTimeForHostTime:CACurrentMediaTime()]; + if ([self.videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { + _lastKnownAvailableTime = outputItemTime; + reportFrame = YES; + } + } + if (reportFrame) { [_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 +@interface FVPDefaultAVFactory : NSObject @end -@implementation FVPDefaultPlayerFactory +@implementation FVPDefaultAVFactory - (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { return [AVPlayer playerWithPlayerItem:playerItem]; } +- (AVPlayerItemVideoOutput *)videoOutputWithPixelBufferAttributes: + (NSDictionary *)attributes { + return [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes]; +} +@end +/** Non-test implementation of the diplay link factory. */ +@interface FVPDefaultDisplayLinkFactory : NSObject @end -@interface FVPVideoPlayer : NSObject -@property(readonly, nonatomic) AVPlayer *player; +@implementation FVPDefaultDisplayLinkFactory +- (FVPDisplayLink *)displayLinkWithRegistrar:(id)registrar + callback:(void (^)(void))callback { + return [[FVPDisplayLink alloc] initWithRegistrar:registrar callback:callback]; +} + +@end + +#pragma mark - + +@interface FVPVideoPlayer () @property(readonly, nonatomic) AVPlayerItemVideoOutput *videoOutput; -// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 -// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some video -// streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). -// 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; // The plugin registrar, to obtain view information from. @property(nonatomic, weak) NSObject *registrar; // The CALayer associated with the Flutter view this plugin is associated with, if any. @@ -91,21 +99,20 @@ @interface FVPVideoPlayer : NSObject @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 +// The updater that drives callbacks to the engine to indicate that a new frame is ready. +@property(nonatomic) FVPFrameUpdater *frameUpdater; +// The display link that drives frameUpdater. +@property(nonatomic) FVPDisplayLink *displayLink; +// Whether a new frame needs to be provided to the engine regardless of the current play/pause state +// (e.g., after a seek while paused). If YES, the display link should continue to run until the next +// frame is successfully provided. +@property(nonatomic, assign) BOOL waitingForFrame; - (instancetype)initWithURL:(NSURL *)url frameUpdater:(FVPFrameUpdater *)frameUpdater + displayLink:(FVPDisplayLink *)displayLink httpHeaders:(nonnull NSDictionary *)headers - playerFactory:(id)playerFactory + avFactory:(id)avFactory registrar:(NSObject *)registrar; @end @@ -119,7 +126,8 @@ - (instancetype)initWithURL:(NSURL *)url @implementation FVPVideoPlayer - (instancetype)initWithAsset:(NSString *)asset frameUpdater:(FVPFrameUpdater *)frameUpdater - playerFactory:(id)playerFactory + displayLink:(FVPDisplayLink *)displayLink + avFactory:(id)avFactory registrar:(NSObject *)registrar { NSString *path = [[NSBundle mainBundle] pathForResource:asset ofType:nil]; #if TARGET_OS_OSX @@ -131,8 +139,9 @@ - (instancetype)initWithAsset:(NSString *)asset #endif return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater + displayLink:displayLink httpHeaders:@{} - playerFactory:playerFactory + avFactory:avFactory registrar:registrar]; } @@ -243,40 +252,11 @@ - (AVMutableVideoComposition *)getVideoCompositionWithTransform:(CGAffineTransfo return videoComposition; } -- (void)createVideoOutputAndDisplayLink:(FVPFrameUpdater *)frameUpdater { - NSDictionary *pixBuffAttributes = @{ - (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), - (id)kCVPixelBufferIOSurfacePropertiesKey : @{} - }; - _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 + displayLink:(FVPDisplayLink *)displayLink httpHeaders:(nonnull NSDictionary *)headers - playerFactory:(id)playerFactory + avFactory:(id)avFactory registrar:(NSObject *)registrar { NSDictionary *options = nil; if ([headers count] != 0) { @@ -286,18 +266,21 @@ - (instancetype)initWithURL:(NSURL *)url AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset]; return [self initWithPlayerItem:item frameUpdater:frameUpdater - playerFactory:playerFactory + displayLink:(FVPDisplayLink *)displayLink + avFactory:avFactory registrar:registrar]; } - (instancetype)initWithPlayerItem:(AVPlayerItem *)item frameUpdater:(FVPFrameUpdater *)frameUpdater - playerFactory:(id)playerFactory + displayLink:(FVPDisplayLink *)displayLink + avFactory:(id)avFactory registrar:(NSObject *)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registrar = registrar; + _frameUpdater = frameUpdater; AVAsset *asset = [item asset]; void (^assetCompletionHandler)(void) = ^{ @@ -328,7 +311,7 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item } }; - _player = [playerFactory playerWithPlayerItem:item]; + _player = [avFactory playerWithPlayerItem:item]; _player.actionAtItemEnd = AVPlayerActionAtItemEndNone; // This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 @@ -339,7 +322,18 @@ - (instancetype)initWithPlayerItem:(AVPlayerItem *)item _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player]; [self.flutterViewLayer addSublayer:_playerLayer]; - [self createVideoOutputAndDisplayLink:frameUpdater]; + // Configure output. + _displayLink = displayLink; + NSDictionary *pixBuffAttributes = @{ + (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), + (id)kCVPixelBufferIOSurfacePropertiesKey : @{} + }; + _videoOutput = [avFactory videoOutputWithPixelBufferAttributes:pixBuffAttributes]; + frameUpdater.videoOutput = _videoOutput; +#if TARGET_OS_IOS + // See TODO on this property in FVPFrameUpdater. + frameUpdater.skipBufferAvailabilityCheck = YES; +#endif [self addObserversForItem:item player:_player]; @@ -422,23 +416,7 @@ - (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 + _displayLink.running = _isPlaying; } - (void)setupEventSinkIfReadyToPlay { @@ -513,16 +491,32 @@ - (int64_t)duration { } - (void)seekTo:(int64_t)location completionHandler:(void (^)(BOOL))completionHandler { - CMTime locationCMT = CMTimeMake(location, 1000); + CMTime previousCMTime = _player.currentTime; + CMTime targetCMTime = CMTimeMake(location, 1000); CMTimeValue duration = _player.currentItem.asset.duration.value; // Without adding tolerance when seeking to duration, // seekToTime will never complete, and this call will hang. // see issue https://github.com/flutter/flutter/issues/124475. CMTime tolerance = location == duration ? CMTimeMake(1, 1000) : kCMTimeZero; - [_player seekToTime:locationCMT + [_player seekToTime:targetCMTime toleranceBefore:tolerance toleranceAfter:tolerance - completionHandler:completionHandler]; + completionHandler:^(BOOL completed) { + if (CMTimeCompare(self.player.currentTime, previousCMTime) != 0) { + // Ensure that a frame is drawn once available, even if currently paused. In theory a race + // is possible here where the new frame has already drawn by the time this code runs, and + // the display link stays on indefinitely, but that should be relatively harmless. This + // must use the display link rather than just informing the engine that a new frame is + // available because the seek completing doesn't guarantee that the pixel buffer is + // already available. + self.waitingForFrame = YES; + self.displayLink.running = YES; + } + + if (completionHandler) { + completionHandler(completed); + } + }]; } - (void)setIsLooping:(BOOL)isLooping { @@ -558,12 +552,29 @@ - (void)setPlaybackSpeed:(double)speed { } - (CVPixelBufferRef)copyPixelBuffer { + CVPixelBufferRef buffer = NULL; CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { - return [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; + buffer = [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; } else { - return NULL; + // If the current time isn't available yet, use the time that was checked when informing the + // engine that a frame was available (if any). + CMTime lastAvailableTime = self.frameUpdater.lastKnownAvailableTime; + if (CMTIME_IS_VALID(lastAvailableTime)) { + buffer = [_videoOutput copyPixelBufferForItemTime:lastAvailableTime itemTimeForDisplay:NULL]; + } + } + + if (self.waitingForFrame && buffer) { + self.waitingForFrame = NO; + // If the display link was only running temporarily to pick up a new frame while the video was + // paused, stop it again. + if (!self.isPlaying) { + self.displayLink.running = NO; + } } + + return buffer; } - (void)onTextureUnregistered:(NSObject *)texture { @@ -603,16 +614,7 @@ - (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]; -#endif + _displayLink = nil; [self removeKeyValueObservers]; [self.player replaceCurrentItemWithPlayerItem:nil]; @@ -653,13 +655,12 @@ - (void)removeKeyValueObservers { @end -@interface FVPVideoPlayerPlugin () +@interface FVPVideoPlayerPlugin () @property(readonly, weak, nonatomic) NSObject *registry; @property(readonly, weak, nonatomic) NSObject *messenger; -@property(readonly, strong, nonatomic) - NSMutableDictionary *playersByTextureId; @property(readonly, strong, nonatomic) NSObject *registrar; -@property(nonatomic, strong) id playerFactory; +@property(nonatomic, strong) id displayLinkFactory; +@property(nonatomic, strong) id avFactory; @end @implementation FVPVideoPlayerPlugin @@ -674,17 +675,21 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (instancetype)initWithRegistrar:(NSObject *)registrar { - return [self initWithPlayerFactory:[[FVPDefaultPlayerFactory alloc] init] registrar:registrar]; + return [self initWithAVFactory:[[FVPDefaultAVFactory alloc] init] + displayLinkFactory:[[FVPDefaultDisplayLinkFactory alloc] init] + registrar:registrar]; } -- (instancetype)initWithPlayerFactory:(id)playerFactory - registrar:(NSObject *)registrar { +- (instancetype)initWithAVFactory:(id)avFactory + displayLinkFactory:(id)displayLinkFactory + registrar:(NSObject *)registrar { self = [super init]; NSAssert(self, @"super init cannot be nil"); _registry = [registrar textures]; _messenger = [registrar messenger]; _registrar = registrar; - _playerFactory = playerFactory; + _displayLinkFactory = displayLinkFactory ?: [[FVPDefaultDisplayLinkFactory alloc] init]; + _avFactory = avFactory ?: [[FVPDefaultAVFactory alloc] init]; _playersByTextureId = [NSMutableDictionary dictionaryWithCapacity:1]; return self; } @@ -729,6 +734,12 @@ - (void)initialize:(FlutterError *__autoreleasing *)error { - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)error { FVPFrameUpdater *frameUpdater = [[FVPFrameUpdater alloc] initWithRegistry:_registry]; + FVPDisplayLink *displayLink = + [self.displayLinkFactory displayLinkWithRegistrar:_registrar + callback:^() { + [frameUpdater displayLinkFired]; + }]; + FVPVideoPlayer *player; if (input.asset) { NSString *assetPath; @@ -740,7 +751,8 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e @try { player = [[FVPVideoPlayer alloc] initWithAsset:assetPath frameUpdater:frameUpdater - playerFactory:_playerFactory + displayLink:displayLink + avFactory:_avFactory registrar:self.registrar]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } @catch (NSException *exception) { @@ -750,8 +762,9 @@ - (FVPTextureMessage *)create:(FVPCreateMessage *)input error:(FlutterError **)e } else if (input.uri) { player = [[FVPVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] frameUpdater:frameUpdater + displayLink:displayLink httpHeaders:input.httpHeaders - playerFactory:_playerFactory + avFactory:_avFactory registrar:self.registrar]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else { @@ -816,7 +829,6 @@ - (void)seekTo:(FVPPositionMessage *)input [player seekTo:input.position completionHandler:^(BOOL finished) { dispatch_async(dispatch_get_main_queue(), ^{ - [self.registry textureFrameAvailable:input.textureId]; completion(nil); }); }]; diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin_Test.h b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin_Test.h index a1e2804bb5d..e34ce16eabc 100644 --- a/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin_Test.h +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin_Test.h @@ -6,13 +6,48 @@ #import -// Protocol for an AVPlayer instance factory. Used for injecting players in tests. -@protocol FVPPlayerFactory +#import "FVPDisplayLink.h" +#import "messages.g.h" + +// Protocol for AVFoundation object instance factory. Used for injecting framework objects in tests. +@protocol FVPAVFactory +@required - (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem; +- (AVPlayerItemVideoOutput *)videoOutputWithPixelBufferAttributes: + (NSDictionary *)attributes; +@end + +// Protocol for an AVPlayer instance factory. Used for injecting display links in tests. +@protocol FVPDisplayLinkFactory +- (FVPDisplayLink *)displayLinkWithRegistrar:(id)registrar + callback:(void (^)(void))callback; +@end + +#pragma mark - + +// TODO(stuartmorgan): Move this whole class to its own files. +@interface FVPVideoPlayer : NSObject +@property(readonly, nonatomic) AVPlayer *player; +// This is to fix 2 bugs: 1. blank video for encrypted video streams on iOS 16 +// (https://github.com/flutter/flutter/issues/111457) and 2. swapped width and height for some video +// streams (not just iOS 16). (https://github.com/flutter/flutter/issues/109116). +// 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; +@property(readonly, nonatomic) int64_t position; + +- (void)onTextureUnregistered:(NSObject *)texture; @end -@interface FVPVideoPlayerPlugin () +#pragma mark - + +@interface FVPVideoPlayerPlugin () + +@property(readonly, strong, nonatomic) + NSMutableDictionary *playersByTextureId; + +- (instancetype)initWithAVFactory:(id)avFactory + displayLinkFactory:(id)displayLinkFactory + registrar:(NSObject *)registrar; -- (instancetype)initWithPlayerFactory:(id)playerFactory - registrar:(NSObject *)registrar; @end diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/ios/FVPDisplayLink.m b/packages/video_player/video_player_avfoundation/darwin/Classes/ios/FVPDisplayLink.m new file mode 100644 index 00000000000..51039067f38 --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/ios/FVPDisplayLink.m @@ -0,0 +1,72 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "../FVPDisplayLink.h" + +#import +#import + +/** + * A proxy object to act as a CADisplayLink target, to avoid retain loops, since FVPDisplayLink + * owns its CADisplayLink, but CADisplayLink retains its target. + */ +@interface FVPDisplayLinkTarget : NSObject +@property(nonatomic) void (^callback)(void); + +/** Initializes a target object that runs the given callback when onDisplayLink: is called. */ +- (instancetype)initWithCallback:(void (^)(void))callback; + +/** Method to be called when a CADisplayLink fires. */ +- (void)onDisplayLink:(CADisplayLink *)link; +@end + +@implementation FVPDisplayLinkTarget +- (instancetype)initWithCallback:(void (^)(void))callback { + self = [super init]; + if (self) { + _callback = callback; + } + return self; +} + +- (void)onDisplayLink:(CADisplayLink *)link { + self.callback(); +} +@end + +#pragma mark - + +@interface FVPDisplayLink () +// The underlying display link implementation. +@property(nonatomic) CADisplayLink *displayLink; +@property(nonatomic) FVPDisplayLinkTarget *target; +@end + +@implementation FVPDisplayLink + +- (instancetype)initWithRegistrar:(id)registrar + callback:(void (^)(void))callback { + self = [super init]; + if (self) { + _target = [[FVPDisplayLinkTarget alloc] initWithCallback:callback]; + _displayLink = [CADisplayLink displayLinkWithTarget:_target selector:@selector(onDisplayLink:)]; + [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; + _displayLink.paused = YES; + } + return self; +} + +- (void)dealloc { + [_displayLink invalidate]; +} + +- (BOOL)running { + return !self.displayLink.paused; +} + +- (void)setRunning:(BOOL)running { + self.displayLink.paused = !running; +} + +@end diff --git a/packages/video_player/video_player_avfoundation/darwin/Classes/macos/FVPDisplayLink.m b/packages/video_player/video_player_avfoundation/darwin/Classes/macos/FVPDisplayLink.m new file mode 100644 index 00000000000..3904c8a288a --- /dev/null +++ b/packages/video_player/video_player_avfoundation/darwin/Classes/macos/FVPDisplayLink.m @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "../FVPDisplayLink.h" + +#import +#import + +@interface FVPDisplayLink () +// The underlying display link implementation. +@property(nonatomic, assign) CVDisplayLinkRef displayLink; +// A dispatch source to move display link callbacks to the main thread. +@property(nonatomic, strong) dispatch_source_t displayLinkSource; +// The plugin registrar, to get screen information. +@property(nonatomic, weak) NSObject *registrar; +@end + +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 the callback there. + __weak dispatch_source_t source = (__bridge dispatch_source_t)displayLinkSource; + dispatch_source_merge_data(source, 1); + return kCVReturnSuccess; +} + +@implementation FVPDisplayLink + +- (instancetype)initWithRegistrar:(id)registrar + callback:(void (^)(void))callback { + self = [super init]; + if (self) { + _registrar = registrar; + // Create and start the main-thread dispatch queue to drive frameUpdater. + _displayLinkSource = + dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_event_handler(_displayLinkSource, ^() { + @autoreleasepool { + callback(); + } + }); + dispatch_resume(_displayLinkSource); + if (CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink) == kCVReturnSuccess) { + CVDisplayLinkSetOutputCallback(_displayLink, &DisplayLinkCallback, + (__bridge void *)(_displayLinkSource)); + } + } + return self; +} + +- (void)dealloc { + CVDisplayLinkStop(_displayLink); + CVDisplayLinkRelease(_displayLink); + _displayLink = NULL; + + dispatch_source_cancel(_displayLinkSource); +} + +- (BOOL)running { + return CVDisplayLinkIsRunning(self.displayLink); +} + +- (void)setRunning:(BOOL)running { + if (self.running == running) { + return; + } + if (running) { + // TODO(stuartmorgan): Move this to init + a screen change listener; this won't correctly + // handle windows being dragged to another screen until the next pause/play cycle. That will + // likely require new plugin registrar APIs. + NSScreen *screen = self.registrar.view.window.screen; + if (screen) { + CGDirectDisplayID viewDisplayID = + (CGDirectDisplayID)[screen.deviceDescription[@"NSScreenNumber"] unsignedIntegerValue]; + CVDisplayLinkSetCurrentCGDisplay(self.displayLink, viewDisplayID); + } + CVDisplayLinkStart(self.displayLink); + } else { + CVDisplayLinkStop(self.displayLink); + } +} + +@end 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 4b030cc6806..00836192a5f 100644 --- a/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m +++ b/packages/video_player/video_player_avfoundation/darwin/RunnerTests/VideoPlayerTests.m @@ -19,19 +19,6 @@ #endif } -@interface FVPVideoPlayer : NSObject -@property(readonly, nonatomic) AVPlayer *player; -@property(readonly, nonatomic) AVPlayerLayer *playerLayer; -@property(readonly, nonatomic) int64_t position; - -- (void)onTextureUnregistered:(NSObject *)texture; -@end - -@interface FVPVideoPlayerPlugin (Test) -@property(readonly, strong, nonatomic) - NSMutableDictionary *playersByTextureId; -@end - #if TARGET_OS_IOS @interface FakeAVAssetTrack : AVAssetTrack @property(readonly, nonatomic) CGAffineTransform preferredTransform; @@ -78,6 +65,7 @@ @interface VideoPlayerTests : XCTestCase @interface StubAVPlayer : AVPlayer @property(readonly, nonatomic) NSNumber *beforeTolerance; @property(readonly, nonatomic) NSNumber *afterTolerance; +@property(readonly, assign) CMTime lastSeekTime; @end @implementation StubAVPlayer @@ -88,33 +76,87 @@ - (void)seekToTime:(CMTime)time completionHandler:(void (^)(BOOL finished))completionHandler { _beforeTolerance = [NSNumber numberWithLong:toleranceBefore.value]; _afterTolerance = [NSNumber numberWithLong:toleranceAfter.value]; - completionHandler(YES); + _lastSeekTime = time; + [super seekToTime:time + toleranceBefore:toleranceBefore + toleranceAfter:toleranceAfter + completionHandler:completionHandler]; } @end -@interface StubFVPPlayerFactory : NSObject +@interface StubFVPAVFactory : NSObject @property(nonatomic, strong) StubAVPlayer *stubAVPlayer; +@property(nonatomic, strong) AVPlayerItemVideoOutput *output; -- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer; +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer + output:(AVPlayerItemVideoOutput *)output; @end -@implementation StubFVPPlayerFactory +@implementation StubFVPAVFactory -- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer { +// Creates a factory that returns the given items. Any items that are nil will instead return +// a real object just as the non-test implementation would. +- (instancetype)initWithPlayer:(StubAVPlayer *)stubAVPlayer + output:(AVPlayerItemVideoOutput *)output { self = [super init]; _stubAVPlayer = stubAVPlayer; + _output = output; return self; } - (AVPlayer *)playerWithPlayerItem:(AVPlayerItem *)playerItem { - return _stubAVPlayer; + return _stubAVPlayer ?: [AVPlayer playerWithPlayerItem:playerItem]; +} + +- (AVPlayerItemVideoOutput *)videoOutputWithPixelBufferAttributes: + (NSDictionary *)attributes { + return _output ?: [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attributes]; +} + +@end + +#pragma mark - + +/** Test implementation of FVPDisplayLinkFactory that returns a provided display link nstance. */ +@interface StubFVPDisplayLinkFactory : NSObject + +/** This display link to return. */ +@property(nonatomic, strong) FVPDisplayLink *displayLink; + +- (instancetype)initWithDisplayLink:(FVPDisplayLink *)displayLink; + +@end + +@implementation StubFVPDisplayLinkFactory +- (instancetype)initWithDisplayLink:(FVPDisplayLink *)displayLink { + self = [super init]; + _displayLink = displayLink; + return self; +} +- (FVPDisplayLink *)displayLinkWithRegistrar:(id)registrar + callback:(void (^)(void))callback { + return self.displayLink; +} + +@end + +/** Non-test implementation of the diplay link factory. */ +@interface FVPDefaultDisplayLinkFactory : NSObject +@end + +@implementation FVPDefaultDisplayLinkFactory +- (FVPDisplayLink *)displayLinkWithRegistrar:(id)registrar + callback:(void (^)(void))callback { + return [[FVPDisplayLink alloc] initWithRegistrar:registrar callback:callback]; } @end +#pragma mark - + @implementation VideoPlayerTests - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSomeVideoStream { @@ -148,15 +190,89 @@ - (void)testBlankVideoBugWithEncryptedVideoStreamAndInvertedAspectRatioBugForSom XCTAssertNotNil(player.playerLayer.superlayer, @"AVPlayerLayer should be added on screen."); } -- (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { +- (void)testSeekToWhilePausedStartsDisplayLinkTemporarily { NSObject *mockTextureRegistry = OCMProtocolMock(@protocol(FlutterTextureRegistry)); NSObject *registrar = - [GetPluginRegistry() registrarForPlugin:@"SeekToInvokestextureFrameAvailable"]; + [GetPluginRegistry() registrarForPlugin:@"SeekToWhilePausedStartsDisplayLinkTemporarily"]; NSObject *partialRegistrar = OCMPartialMock(registrar); OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); - FVPVideoPlayerPlugin *videoPlayerPlugin = - (FVPVideoPlayerPlugin *)[[FVPVideoPlayerPlugin alloc] initWithRegistrar:partialRegistrar]; + FVPDisplayLink *mockDisplayLink = + OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar + callback:^(){ + }]); + StubFVPDisplayLinkFactory *stubDisplayLinkFactory = + [[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink]; + AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]); + FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] + initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput] + displayLinkFactory:stubDisplayLinkFactory + registrar:partialRegistrar]; + + FlutterError *initalizationError; + [videoPlayerPlugin initialize:&initalizationError]; + XCTAssertNil(initalizationError); + FVPCreateMessage *create = [FVPCreateMessage + makeWithAsset:nil + uri:@"https://flutter.github.io/assets-for-api-docs/assets/videos/hls/bee.m3u8" + packageName:nil + formatHint:nil + httpHeaders:@{}]; + FlutterError *createError; + FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError]; + NSInteger textureId = textureMessage.textureId; + + // Ensure that the video playback is paused before seeking. + FlutterError *pauseError; + [videoPlayerPlugin pause:textureMessage error:&pauseError]; + + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo completes"]; + FVPPositionMessage *message = [FVPPositionMessage makeWithTextureId:textureId position:1234]; + [videoPlayerPlugin seekTo:message + completion:^(FlutterError *_Nullable error) { + [initializedExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:30.0 handler:nil]; + + // Seeking to a new position should start the display link temporarily. + OCMVerify([mockDisplayLink setRunning:YES]); + + FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[@(textureId)]; + XCTAssertEqual([player position], 1234); + + // Simulate a buffer being available. + OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero]) + .ignoringNonObjectArgs() + .andReturn(YES); + // Any non-zero value is fine here since it won't actually be used, just NULL-checked. + CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1; + OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL]) + .ignoringNonObjectArgs() + .andReturn(fakeBufferRef); + // Simulate a callback from the engine to request a new frame. + [player copyPixelBuffer]; + // Since a frame was found, and the video is paused, the display link should be paused again. + OCMVerify([mockDisplayLink setRunning:NO]); +} + +- (void)testSeekToWhilePlayingDoesNotStopDisplayLink { + NSObject *mockTextureRegistry = + OCMProtocolMock(@protocol(FlutterTextureRegistry)); + NSObject *registrar = + [GetPluginRegistry() registrarForPlugin:@"SeekToWhilePlayingDoesNotStopDisplayLink"]; + NSObject *partialRegistrar = OCMPartialMock(registrar); + OCMStub([partialRegistrar textures]).andReturn(mockTextureRegistry); + FVPDisplayLink *mockDisplayLink = + OCMPartialMock([[FVPDisplayLink alloc] initWithRegistrar:registrar + callback:^(){ + }]); + StubFVPDisplayLinkFactory *stubDisplayLinkFactory = + [[StubFVPDisplayLinkFactory alloc] initWithDisplayLink:mockDisplayLink]; + AVPlayerItemVideoOutput *mockVideoOutput = OCMPartialMock([[AVPlayerItemVideoOutput alloc] init]); + FVPVideoPlayerPlugin *videoPlayerPlugin = [[FVPVideoPlayerPlugin alloc] + initWithAVFactory:[[StubFVPAVFactory alloc] initWithPlayer:nil output:mockVideoOutput] + displayLinkFactory:stubDisplayLinkFactory + registrar:partialRegistrar]; FlutterError *initalizationError; [videoPlayerPlugin initialize:&initalizationError]; @@ -171,6 +287,10 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { FVPTextureMessage *textureMessage = [videoPlayerPlugin create:create error:&createError]; NSInteger textureId = textureMessage.textureId; + // Ensure that the video is playing before seeking. + FlutterError *pauseError; + [videoPlayerPlugin play:textureMessage error:&pauseError]; + XCTestExpectation *initializedExpectation = [self expectationWithDescription:@"seekTo completes"]; FVPPositionMessage *message = [FVPPositionMessage makeWithTextureId:textureId position:1234]; [videoPlayerPlugin seekTo:message @@ -178,10 +298,24 @@ - (void)testSeekToInvokesTextureFrameAvailableOnTextureRegistry { [initializedExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:30.0 handler:nil]; - OCMVerify([mockTextureRegistry textureFrameAvailable:message.textureId]); + OCMVerify([mockDisplayLink setRunning:YES]); FVPVideoPlayer *player = videoPlayerPlugin.playersByTextureId[@(textureId)]; XCTAssertEqual([player position], 1234); + + // Simulate a buffer being available. + OCMStub([mockVideoOutput hasNewPixelBufferForItemTime:kCMTimeZero]) + .ignoringNonObjectArgs() + .andReturn(YES); + // Any non-zero value is fine here since it won't actually be used, just NULL-checked. + CVPixelBufferRef fakeBufferRef = (CVPixelBufferRef)1; + OCMStub([mockVideoOutput copyPixelBufferForItemTime:kCMTimeZero itemTimeForDisplay:NULL]) + .ignoringNonObjectArgs() + .andReturn(fakeBufferRef); + // Simulate a callback from the engine to request a new frame. + [player copyPixelBuffer]; + // Since the video was playing, the display link should not be paused after getting a buffer. + OCMVerify(never(), [mockDisplayLink setRunning:NO]); } - (void)testDeregistersFromPlayer { @@ -323,10 +457,12 @@ - (void)testSeekToleranceWhenNotSeekingToEnd { [GetPluginRegistry() registrarForPlugin:@"TestSeekTolerance"]; StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; - StubFVPPlayerFactory *stubFVPPlayerFactory = - [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + StubFVPAVFactory *stubAVFactory = [[StubFVPAVFactory alloc] initWithPlayer:stubAVPlayer + output:nil]; FVPVideoPlayerPlugin *pluginWithMockAVPlayer = - [[FVPVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + [[FVPVideoPlayerPlugin alloc] initWithAVFactory:stubAVFactory + displayLinkFactory:nil + registrar:registrar]; FlutterError *initializationError; [pluginWithMockAVPlayer initialize:&initializationError]; @@ -360,10 +496,12 @@ - (void)testSeekToleranceWhenSeekingToEnd { [GetPluginRegistry() registrarForPlugin:@"TestSeekToEndTolerance"]; StubAVPlayer *stubAVPlayer = [[StubAVPlayer alloc] init]; - StubFVPPlayerFactory *stubFVPPlayerFactory = - [[StubFVPPlayerFactory alloc] initWithPlayer:stubAVPlayer]; + StubFVPAVFactory *stubAVFactory = [[StubFVPAVFactory alloc] initWithPlayer:stubAVPlayer + output:nil]; FVPVideoPlayerPlugin *pluginWithMockAVPlayer = - [[FVPVideoPlayerPlugin alloc] initWithPlayerFactory:stubFVPPlayerFactory registrar:registrar]; + [[FVPVideoPlayerPlugin alloc] initWithAVFactory:stubAVFactory + displayLinkFactory:nil + registrar:registrar]; FlutterError *initializationError; [pluginWithMockAVPlayer initialize:&initializationError]; diff --git a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation.podspec b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation.podspec index 3f873fc9a40..e0a46b9b12f 100644 --- a/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation.podspec +++ b/packages/video_player/video_player_avfoundation/darwin/video_player_avfoundation.podspec @@ -14,7 +14,9 @@ Downloaded by pub (not CocoaPods). s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/video_player/video_player_avfoundation' } s.documentation_url = 'https://pub.dev/packages/video_player' - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/*' + s.ios.source_files = 'Classes/ios/*' + s.osx.source_files = 'Classes/macos/*' s.public_header_files = 'Classes/**/*.h' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' diff --git a/packages/video_player/video_player_avfoundation/example/ios/Podfile b/packages/video_player/video_player_avfoundation/example/ios/Podfile index 9e843931a57..3c06e851673 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Podfile +++ b/packages/video_player/video_player_avfoundation/example/ios/Podfile @@ -31,7 +31,7 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths - pod 'OCMock', '3.5' + pod 'OCMock', '3.9.1' end end diff --git a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj index b43b5503c9c..0fb67b60954 100644 --- a/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/video_player/video_player_avfoundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33683FF02ABCAC94007305E4 /* VideoPlayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VideoPlayerTests.m; path = ../../../darwin/RunnerTests/VideoPlayerTests.m; sourceTree = ""; }; + 33683FF02ABCAC94007305E4 /* VideoPlayerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VideoPlayerTests.m; path = ../../darwin/RunnerTests/VideoPlayerTests.m; sourceTree = SOURCE_ROOT; }; 33CC10ED2044A3C60003C045 /* video_player_avfoundation_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = video_player_avfoundation_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 38c4658f4b8..e10c729efa1 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.5.1 +version: 2.5.2 environment: sdk: ">=3.1.0 <4.0.0"