diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index 9147872b76..c10a5f621a 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -7,25 +7,28 @@ typedef NS_ENUM(NSUInteger, ASVideoGravity) { ASVideoGravityResize }; +@protocol ASVideoNodeDelegate; + @interface ASVideoNode : ASControlNode<_ASDisplayLayerDelegate> @property (atomic, strong, readwrite) AVAsset *asset; +@property (atomic, strong, readonly) AVPlayer *player; +@property (atomic, strong, readonly) AVPlayerItem *currentItem; @property (nonatomic, assign, readwrite) BOOL shouldAutoplay; @property (atomic) ASVideoGravity gravity; @property (atomic) BOOL autorepeat; @property (atomic) ASButtonNode *playButton; -@property (atomic) AVPlayer *player; + +@property (atomic, weak, readwrite) id delegate; - (void)play; - (void)pause; +- (BOOL)isPlaying; + @end @protocol ASVideoNodeDelegate +@optional +- (void)videoDidReachEnd:(ASVideoNode *)videoNode; @end -@protocol ASVideoNodeDataSource -@optional -- (ASButtonNode *)playButtonForVideoNode:(ASVideoNode *)videoNode; -- (UIImage *)thumbnailForVideoNode:(ASVideoNode *) videoNode; -- (NSURL *)thumbnailURLForVideoNode:(ASVideoNode *)videoNode; -@end diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 148aa27ae8..be40e950a8 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -6,14 +6,18 @@ { ASDN::RecursiveMutex _lock; - __weak id _dataSource; - + __weak id _delegate; + BOOL _shouldBePlaying; AVAsset *_asset; + AVPlayerItem *_currentItem; + AVPlayer *_player; + ASButtonNode *_playButton; ASDisplayNode *_playerNode; ASDisplayNode *_spinner; + ASVideoGravity _gravity; } @end @@ -28,13 +32,6 @@ { if (!(self = [super init])) { return nil; } - _playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ - AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; - playerLayer.player = [[AVPlayer alloc] init]; - return playerLayer; - }]; - [self addSubnode:_playerNode]; - self.gravity = ASVideoGravityResizeAspect; [self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; @@ -51,12 +48,17 @@ if (!(newState & ASInterfaceStateVisible)) { [self pause]; + [(UIActivityIndicatorView *)_spinner.view stopAnimating]; [_spinner removeFromSupernode]; } else { if (_shouldBePlaying) { [self play]; } } + + if (newState & ASInterfaceStateVisible) { + [self displayDidFinish]; + } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context @@ -76,7 +78,8 @@ - (void)didPlayToEnd:(NSNotification *)notification { if (ASObjectIsEqual([[notification object] asset], _asset)) { - [[((AVPlayerLayer *)_playerNode.layer) player] seekToTime:CMTimeMakeWithSeconds(0, 1)]; + [_delegate videoDidReachEnd:self]; + [_player seekToTime:CMTimeMakeWithSeconds(0, 1)]; if (_autorepeat) { [self play]; @@ -90,6 +93,12 @@ { [super layout]; _playerNode.frame = self.bounds; + + CGFloat horizontalDiff = (self.bounds.size.width - _playButton.bounds.size.width)/2; + CGFloat verticalDiff = (self.bounds.size.height - _playButton.bounds.size.height)/2; + _playButton.hitTestSlop = UIEdgeInsetsMake(-verticalDiff, -horizontalDiff, -verticalDiff, -horizontalDiff); + + _spinner.frame = _playButton.frame; } - (void)tapped @@ -123,18 +132,47 @@ _currentItem = [[AVPlayerItem alloc] initWithAsset:_asset]; [_currentItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; - if (((AVPlayerLayer *)_playerNode.layer).player) { - [((AVPlayerLayer *)_playerNode.layer).player replaceCurrentItemWithPlayerItem:_currentItem]; + if (_player) { + [_player replaceCurrentItemWithPlayerItem:_currentItem]; } else { - ((AVPlayerLayer *)_playerNode.layer).player = [[AVPlayer alloc] initWithPlayerItem:_currentItem]; + _player = [[AVPlayer alloc] initWithPlayerItem:_currentItem]; } - } +} + +// FIXME: Adopt interfaceStateDidChange API +- (void)displayDidFinish +{ + [super displayDidFinish]; + + ASDN::MutexLocker l(_lock); + + _playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ + AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; + playerLayer.player = _player; + playerLayer.videoGravity = [self videoGravity]; + return playerLayer; + }]; + + [self insertSubnode:_playerNode atIndex:0]; + if (_shouldAutoplay) { [self play]; } } +- (NSString *)videoGravity +{ + if (_gravity == ASVideoGravityResize) { + return AVLayerVideoGravityResize; + } + if (_gravity == ASVideoGravityResizeAspectFill) { + return AVLayerVideoGravityResizeAspectFill; + } + + return AVLayerVideoGravityResizeAspect; +} + - (void)clearFetchedData { [super clearFetchedData]; @@ -142,10 +180,13 @@ { ASDN::MutexLocker l(_lock); ((AVPlayerLayer *)_playerNode.layer).player = nil; + _player = nil; _shouldBePlaying = NO; } } +#pragma mark - Video Properties + - (void)setPlayButton:(ASButtonNode *)playButton { ASDN::MutexLocker l(_lock); @@ -174,6 +215,7 @@ _asset = asset; + // FIXME: Adopt -setNeedsFetchData when it is available if (self.interfaceState & ASInterfaceStateFetchData) { [self fetchData]; } @@ -185,6 +227,12 @@ return _asset; } +- (AVPlayer *)player +{ + ASDN::MutexLocker l(_lock); + return _player; +} + - (void)setGravity:(ASVideoGravity)gravity { ASDN::MutexLocker l(_lock); @@ -203,77 +251,92 @@ ((AVPlayerLayer *)_playerNode.layer).videoGravity = AVLayerVideoGravityResizeAspect; break; } + + _gravity = gravity; } - (ASVideoGravity)gravity { ASDN::MutexLocker l(_lock); - if (ASObjectIsEqual(((AVPlayerLayer *)_playerNode.layer).contentsGravity, AVLayerVideoGravityResize)) { - return ASVideoGravityResize; - } - if (ASObjectIsEqual(((AVPlayerLayer *)_playerNode.layer).contentsGravity, AVLayerVideoGravityResizeAspectFill)) { - return ASVideoGravityResizeAspectFill; - } - - return ASVideoGravityResizeAspect; + return _gravity; } +#pragma mark - Video Playback + - (void)play { ASDN::MutexLocker l(_lock); if (!_spinner) { _spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ - UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] initWithFrame:_playButton.frame]; + UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init]; spinnnerView.color = [UIColor whiteColor]; - [spinnnerView startAnimating]; return spinnnerView; }]; } - if (![self ready]) { - [self addSubnode:_spinner]; - } - - [[((AVPlayerLayer *)_playerNode.layer) player] play]; + [_player play]; _shouldBePlaying = YES; _playButton.alpha = 0.0; -// if ([self ready] && ![self.subnodes containsObject:_spinner]) { -// } + + if (![self ready] && _shouldBePlaying && (self.interfaceState & ASInterfaceStateVisible)) { + [self addSubnode:_spinner]; + [(UIActivityIndicatorView *)_spinner.view startAnimating]; + } } - (BOOL)ready { - return [((AVPlayerLayer *)_playerNode.layer) player].currentItem.status == AVPlayerItemStatusReadyToPlay; + return _currentItem.status == AVPlayerItemStatusReadyToPlay; } - (void)pause { ASDN::MutexLocker l(_lock); - [[((AVPlayerLayer *)_playerNode.layer) player] pause]; + [_player pause]; [((UIActivityIndicatorView *)_spinner.view) stopAnimating]; _shouldBePlaying = NO; _playButton.alpha = 1.0; } -- (AVPlayerItem *)currentItem +- (BOOL)isPlaying { - return _currentItem; + ASDN::MutexLocker l(_lock); + + return (_player.rate > 0 && !_player.error); } +#pragma mark - Property Accessors for Tests + - (ASDisplayNode *)spinner { + ASDN::MutexLocker l(_lock); return _spinner; } - (AVPlayerItem *)curentItem { + ASDN::MutexLocker l(_lock); return _currentItem; } +- (ASDisplayNode *)playerNode +{ + ASDN::MutexLocker l(_lock); + return _playerNode; +} + +- (BOOL)shouldBePlaying +{ + ASDN::MutexLocker l(_lock); + return _shouldBePlaying; +} + +#pragma mark - Lifecycle + - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index 709b353b47..a63c0e0e2b 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -11,45 +11,51 @@ #import "ASVideoNode.h" @interface ASVideoNodeTests : XCTestCase +{ + ASVideoNode *_videoNode; + AVAsset *_firstAsset; + AVAsset *_secondAsset; +} @end @interface ASVideoNode () -@property (atomic, readonly) AVPlayerItem *currentItem; @property (atomic) ASInterfaceState interfaceState; @property (atomic) ASDisplayNode *spinner; -@end - -@interface AVPlayerItem () -@property (nonatomic) AVPlayerItemStatus status; +@property (atomic) ASDisplayNode *playerNode; +@property (atomic) BOOL shouldBePlaying; @end @implementation ASVideoNodeTests +- (void)setUp +{ + _videoNode = [[ASVideoNode alloc] init]; + _firstAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; + _secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; +} + - (void)testVideoNodeReplacesAVPlayerItemWhenNewURLIsSet { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - videoNode.interfaceState = ASInterfaceStateFetchData; - videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; + _videoNode.interfaceState = ASInterfaceStateFetchData; + _videoNode.asset = _firstAsset; - AVPlayerItem *item = [videoNode currentItem]; + AVPlayerItem *item = [_videoNode currentItem]; - videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; - AVPlayerItem *secondItem = [videoNode currentItem]; + _videoNode.asset = _secondAsset; + AVPlayerItem *secondItem = [_videoNode currentItem]; XCTAssertNotEqualObjects(item, secondItem); } - (void)testVideoNodeDoesNotReplaceAVPlayerItemWhenSameURLIsSet { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - videoNode.interfaceState = ASInterfaceStateFetchData; - AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; + _videoNode.interfaceState = ASInterfaceStateFetchData; - videoNode.asset = asset; - AVPlayerItem *item = [videoNode currentItem]; + _videoNode.asset = _firstAsset; + AVPlayerItem *item = [_videoNode currentItem]; - videoNode.asset = asset; - AVPlayerItem *secondItem = [videoNode currentItem]; + _videoNode.asset = _firstAsset; + AVPlayerItem *secondItem = [_videoNode currentItem]; XCTAssertEqualObjects(item, secondItem); } @@ -58,53 +64,73 @@ - (void)testSpinnerDefaultsToNil { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - - XCTAssertNil(videoNode.spinner); + XCTAssertNil(_videoNode.spinner); } - (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - videoNode.interfaceState = ASInterfaceStateFetchData; - AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; - videoNode.asset = asset; + _videoNode.interfaceState = ASInterfaceStateFetchData; + _videoNode.asset = _firstAsset; - [videoNode play]; + [_videoNode play]; - XCTAssertNotNil(videoNode.spinner); + XCTAssertNotNil(_videoNode.spinner); } - (void)testOnPauseSpinnerIsPausedIfPresent { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - videoNode.interfaceState = ASInterfaceStateFetchData; - AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; - videoNode.asset = asset; + _videoNode.interfaceState = ASInterfaceStateFetchData; + _videoNode.asset = _firstAsset; - [videoNode play]; + [_videoNode play]; - [videoNode pause]; + [_videoNode pause]; - XCTAssertFalse(((UIActivityIndicatorView *)videoNode.spinner.view).isAnimating); + XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); } - (void)testOnVideoReadySpinnerIsStoppedAndRemoved { - ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - videoNode.interfaceState = ASInterfaceStateFetchData; - AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; - videoNode.asset = asset; + _videoNode.interfaceState = ASInterfaceStateFetchData; + _videoNode.asset = _firstAsset; - [videoNode play]; - [videoNode observeValueForKeyPath:@"status" ofObject:[videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; + [_videoNode play]; + [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; - XCTAssertFalse(((UIActivityIndicatorView *)videoNode.spinner.view).isAnimating); + XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); } -- (void)testPlayButtonUserInteractionIsNotEnabled +- (void)testPlayerDefaultsToNil { + XCTAssertNil(_videoNode.player); +} + +- (void)testPlayerIsCreatedInFetchData +{ + _videoNode.asset = _firstAsset; + + _videoNode.interfaceState = ASInterfaceStateFetchData; + XCTAssertNotNil(_videoNode.player); +} + +- (void)testPlayerLayerNodeIsAddedOnDisplayDidFinish +{ + _videoNode.asset = _firstAsset; + + [_videoNode displayDidFinish]; + + XCTAssert([_videoNode.subnodes containsObject:_videoNode.playerNode]); +} + +- (void)testVideoStartsPlayingOnDidDisplayIfAutoplayIsSet +{ + _videoNode.asset = _firstAsset; + _videoNode.shouldAutoplay = YES; + + [_videoNode displayDidFinish]; + + XCTAssertTrue(_videoNode.shouldBePlaying); } @end diff --git a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7b5a2f3050 --- /dev/null +++ b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/Videos/Sample/ViewController.m b/examples/Videos/Sample/ViewController.m index c941fbbd30..32f3d426af 100644 --- a/examples/Videos/Sample/ViewController.m +++ b/examples/Videos/Sample/ViewController.m @@ -11,7 +11,7 @@ #import "ViewController.h" -@interface ViewController() +@interface ViewController() @property (nonatomic) ASVideoNode *videoNode; @end @@ -51,6 +51,8 @@ { ASVideoNode *nicCageVideo = [[ASVideoNode alloc] init]; + nicCageVideo.delegate = self; + nicCageVideo.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"http://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]]; nicCageVideo.frame = CGRectMake([UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/3, [UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/3); @@ -102,4 +104,9 @@ return YES; } +- (void)videoDidReachEnd:(ASVideoNode *)videoNode +{ + //Do something with your video if you so desire. +} + @end