added delegate method for video did finish, moved layer creation to after view displays, rearranged spinner logic, added tests

This commit is contained in:
Luke Parham
2015-12-22 15:34:11 -06:00
parent e318285089
commit 6dc15ffd44
5 changed files with 194 additions and 85 deletions

View File

@@ -7,25 +7,28 @@ typedef NS_ENUM(NSUInteger, ASVideoGravity) {
ASVideoGravityResize ASVideoGravityResize
}; };
@protocol ASVideoNodeDelegate;
@interface ASVideoNode : ASControlNode<_ASDisplayLayerDelegate> @interface ASVideoNode : ASControlNode<_ASDisplayLayerDelegate>
@property (atomic, strong, readwrite) AVAsset *asset; @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 (nonatomic, assign, readwrite) BOOL shouldAutoplay;
@property (atomic) ASVideoGravity gravity; @property (atomic) ASVideoGravity gravity;
@property (atomic) BOOL autorepeat; @property (atomic) BOOL autorepeat;
@property (atomic) ASButtonNode *playButton; @property (atomic) ASButtonNode *playButton;
@property (atomic) AVPlayer *player;
@property (atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate;
- (void)play; - (void)play;
- (void)pause; - (void)pause;
- (BOOL)isPlaying;
@end @end
@protocol ASVideoNodeDelegate <NSObject> @protocol ASVideoNodeDelegate <NSObject>
@optional
- (void)videoDidReachEnd:(ASVideoNode *)videoNode;
@end @end
@protocol ASVideoNodeDataSource <NSObject>
@optional
- (ASButtonNode *)playButtonForVideoNode:(ASVideoNode *)videoNode;
- (UIImage *)thumbnailForVideoNode:(ASVideoNode *) videoNode;
- (NSURL *)thumbnailURLForVideoNode:(ASVideoNode *)videoNode;
@end

View File

@@ -6,14 +6,18 @@
{ {
ASDN::RecursiveMutex _lock; ASDN::RecursiveMutex _lock;
__weak id<ASVideoNodeDataSource> _dataSource; __weak id<ASVideoNodeDelegate> _delegate;
BOOL _shouldBePlaying; BOOL _shouldBePlaying;
AVAsset *_asset; AVAsset *_asset;
AVPlayerItem *_currentItem; AVPlayerItem *_currentItem;
AVPlayer *_player;
ASButtonNode *_playButton; ASButtonNode *_playButton;
ASDisplayNode *_playerNode; ASDisplayNode *_playerNode;
ASDisplayNode *_spinner; ASDisplayNode *_spinner;
ASVideoGravity _gravity;
} }
@end @end
@@ -28,13 +32,6 @@
{ {
if (!(self = [super init])) { return nil; } 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.gravity = ASVideoGravityResizeAspect;
[self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; [self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
@@ -51,12 +48,17 @@
if (!(newState & ASInterfaceStateVisible)) { if (!(newState & ASInterfaceStateVisible)) {
[self pause]; [self pause];
[(UIActivityIndicatorView *)_spinner.view stopAnimating];
[_spinner removeFromSupernode]; [_spinner removeFromSupernode];
} else { } else {
if (_shouldBePlaying) { if (_shouldBePlaying) {
[self play]; [self play];
} }
} }
if (newState & ASInterfaceStateVisible) {
[self displayDidFinish];
}
} }
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
@@ -76,7 +78,8 @@
- (void)didPlayToEnd:(NSNotification *)notification - (void)didPlayToEnd:(NSNotification *)notification
{ {
if (ASObjectIsEqual([[notification object] asset], _asset)) { if (ASObjectIsEqual([[notification object] asset], _asset)) {
[[((AVPlayerLayer *)_playerNode.layer) player] seekToTime:CMTimeMakeWithSeconds(0, 1)]; [_delegate videoDidReachEnd:self];
[_player seekToTime:CMTimeMakeWithSeconds(0, 1)];
if (_autorepeat) { if (_autorepeat) {
[self play]; [self play];
@@ -90,6 +93,12 @@
{ {
[super layout]; [super layout];
_playerNode.frame = self.bounds; _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 - (void)tapped
@@ -123,18 +132,47 @@
_currentItem = [[AVPlayerItem alloc] initWithAsset:_asset]; _currentItem = [[AVPlayerItem alloc] initWithAsset:_asset];
[_currentItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; [_currentItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
if (((AVPlayerLayer *)_playerNode.layer).player) { if (_player) {
[((AVPlayerLayer *)_playerNode.layer).player replaceCurrentItemWithPlayerItem:_currentItem]; [_player replaceCurrentItemWithPlayerItem:_currentItem];
} else { } 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) { if (_shouldAutoplay) {
[self play]; [self play];
} }
} }
- (NSString *)videoGravity
{
if (_gravity == ASVideoGravityResize) {
return AVLayerVideoGravityResize;
}
if (_gravity == ASVideoGravityResizeAspectFill) {
return AVLayerVideoGravityResizeAspectFill;
}
return AVLayerVideoGravityResizeAspect;
}
- (void)clearFetchedData - (void)clearFetchedData
{ {
[super clearFetchedData]; [super clearFetchedData];
@@ -142,10 +180,13 @@
{ {
ASDN::MutexLocker l(_lock); ASDN::MutexLocker l(_lock);
((AVPlayerLayer *)_playerNode.layer).player = nil; ((AVPlayerLayer *)_playerNode.layer).player = nil;
_player = nil;
_shouldBePlaying = NO; _shouldBePlaying = NO;
} }
} }
#pragma mark - Video Properties
- (void)setPlayButton:(ASButtonNode *)playButton - (void)setPlayButton:(ASButtonNode *)playButton
{ {
ASDN::MutexLocker l(_lock); ASDN::MutexLocker l(_lock);
@@ -174,6 +215,7 @@
_asset = asset; _asset = asset;
// FIXME: Adopt -setNeedsFetchData when it is available
if (self.interfaceState & ASInterfaceStateFetchData) { if (self.interfaceState & ASInterfaceStateFetchData) {
[self fetchData]; [self fetchData];
} }
@@ -185,6 +227,12 @@
return _asset; return _asset;
} }
- (AVPlayer *)player
{
ASDN::MutexLocker l(_lock);
return _player;
}
- (void)setGravity:(ASVideoGravity)gravity - (void)setGravity:(ASVideoGravity)gravity
{ {
ASDN::MutexLocker l(_lock); ASDN::MutexLocker l(_lock);
@@ -203,21 +251,18 @@
((AVPlayerLayer *)_playerNode.layer).videoGravity = AVLayerVideoGravityResizeAspect; ((AVPlayerLayer *)_playerNode.layer).videoGravity = AVLayerVideoGravityResizeAspect;
break; break;
} }
_gravity = gravity;
} }
- (ASVideoGravity)gravity - (ASVideoGravity)gravity
{ {
ASDN::MutexLocker l(_lock); ASDN::MutexLocker l(_lock);
if (ASObjectIsEqual(((AVPlayerLayer *)_playerNode.layer).contentsGravity, AVLayerVideoGravityResize)) { return _gravity;
return ASVideoGravityResize;
}
if (ASObjectIsEqual(((AVPlayerLayer *)_playerNode.layer).contentsGravity, AVLayerVideoGravityResizeAspectFill)) {
return ASVideoGravityResizeAspectFill;
} }
return ASVideoGravityResizeAspect; #pragma mark - Video Playback
}
- (void)play - (void)play
{ {
@@ -225,55 +270,73 @@
if (!_spinner) { if (!_spinner) {
_spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{ _spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] initWithFrame:_playButton.frame]; UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init];
spinnnerView.color = [UIColor whiteColor]; spinnnerView.color = [UIColor whiteColor];
[spinnnerView startAnimating];
return spinnnerView; return spinnnerView;
}]; }];
} }
if (![self ready]) { [_player play];
[self addSubnode:_spinner];
}
[[((AVPlayerLayer *)_playerNode.layer) player] play];
_shouldBePlaying = YES; _shouldBePlaying = YES;
_playButton.alpha = 0.0; _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 - (BOOL)ready
{ {
return [((AVPlayerLayer *)_playerNode.layer) player].currentItem.status == AVPlayerItemStatusReadyToPlay; return _currentItem.status == AVPlayerItemStatusReadyToPlay;
} }
- (void)pause - (void)pause
{ {
ASDN::MutexLocker l(_lock); ASDN::MutexLocker l(_lock);
[[((AVPlayerLayer *)_playerNode.layer) player] pause]; [_player pause];
[((UIActivityIndicatorView *)_spinner.view) stopAnimating]; [((UIActivityIndicatorView *)_spinner.view) stopAnimating];
_shouldBePlaying = NO; _shouldBePlaying = NO;
_playButton.alpha = 1.0; _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 - (ASDisplayNode *)spinner
{ {
ASDN::MutexLocker l(_lock);
return _spinner; return _spinner;
} }
- (AVPlayerItem *)curentItem - (AVPlayerItem *)curentItem
{ {
ASDN::MutexLocker l(_lock);
return _currentItem; return _currentItem;
} }
- (ASDisplayNode *)playerNode
{
ASDN::MutexLocker l(_lock);
return _playerNode;
}
- (BOOL)shouldBePlaying
{
ASDN::MutexLocker l(_lock);
return _shouldBePlaying;
}
#pragma mark - Lifecycle
- (void)dealloc - (void)dealloc
{ {
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];

View File

@@ -11,45 +11,51 @@
#import "ASVideoNode.h" #import "ASVideoNode.h"
@interface ASVideoNodeTests : XCTestCase @interface ASVideoNodeTests : XCTestCase
{
ASVideoNode *_videoNode;
AVAsset *_firstAsset;
AVAsset *_secondAsset;
}
@end @end
@interface ASVideoNode () @interface ASVideoNode ()
@property (atomic, readonly) AVPlayerItem *currentItem;
@property (atomic) ASInterfaceState interfaceState; @property (atomic) ASInterfaceState interfaceState;
@property (atomic) ASDisplayNode *spinner; @property (atomic) ASDisplayNode *spinner;
@end @property (atomic) ASDisplayNode *playerNode;
@property (atomic) BOOL shouldBePlaying;
@interface AVPlayerItem ()
@property (nonatomic) AVPlayerItemStatus status;
@end @end
@implementation ASVideoNodeTests @implementation ASVideoNodeTests
- (void)setUp
{
_videoNode = [[ASVideoNode alloc] init];
_firstAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
_secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]];
}
- (void)testVideoNodeReplacesAVPlayerItemWhenNewURLIsSet - (void)testVideoNodeReplacesAVPlayerItemWhenNewURLIsSet
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; _videoNode.interfaceState = ASInterfaceStateFetchData;
videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset;
videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
AVPlayerItem *item = [videoNode currentItem]; AVPlayerItem *item = [_videoNode currentItem];
videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; _videoNode.asset = _secondAsset;
AVPlayerItem *secondItem = [videoNode currentItem]; AVPlayerItem *secondItem = [_videoNode currentItem];
XCTAssertNotEqualObjects(item, secondItem); XCTAssertNotEqualObjects(item, secondItem);
} }
- (void)testVideoNodeDoesNotReplaceAVPlayerItemWhenSameURLIsSet - (void)testVideoNodeDoesNotReplaceAVPlayerItemWhenSameURLIsSet
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; _videoNode.interfaceState = ASInterfaceStateFetchData;
videoNode.interfaceState = ASInterfaceStateFetchData;
AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
videoNode.asset = asset; _videoNode.asset = _firstAsset;
AVPlayerItem *item = [videoNode currentItem]; AVPlayerItem *item = [_videoNode currentItem];
videoNode.asset = asset; _videoNode.asset = _firstAsset;
AVPlayerItem *secondItem = [videoNode currentItem]; AVPlayerItem *secondItem = [_videoNode currentItem];
XCTAssertEqualObjects(item, secondItem); XCTAssertEqualObjects(item, secondItem);
} }
@@ -58,53 +64,73 @@
- (void)testSpinnerDefaultsToNil - (void)testSpinnerDefaultsToNil
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; XCTAssertNil(_videoNode.spinner);
XCTAssertNil(videoNode.spinner);
} }
- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode - (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; _videoNode.interfaceState = ASInterfaceStateFetchData;
videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset;
AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
videoNode.asset = asset;
[videoNode play]; [_videoNode play];
XCTAssertNotNil(videoNode.spinner); XCTAssertNotNil(_videoNode.spinner);
} }
- (void)testOnPauseSpinnerIsPausedIfPresent - (void)testOnPauseSpinnerIsPausedIfPresent
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; _videoNode.interfaceState = ASInterfaceStateFetchData;
videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset;
AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
videoNode.asset = asset;
[videoNode play]; [_videoNode play];
[videoNode pause]; [_videoNode pause];
XCTAssertFalse(((UIActivityIndicatorView *)videoNode.spinner.view).isAnimating); XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating);
} }
- (void)testOnVideoReadySpinnerIsStoppedAndRemoved - (void)testOnVideoReadySpinnerIsStoppedAndRemoved
{ {
ASVideoNode *videoNode = [[ASVideoNode alloc] init]; _videoNode.interfaceState = ASInterfaceStateFetchData;
videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset;
AVAsset *asset = [AVAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
videoNode.asset = asset;
[videoNode play]; [_videoNode play];
[videoNode observeValueForKeyPath:@"status" ofObject:[videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; [_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 @end

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Sample.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -11,7 +11,7 @@
#import "ViewController.h" #import "ViewController.h"
@interface ViewController() @interface ViewController()<ASVideoNodeDelegate>
@property (nonatomic) ASVideoNode *videoNode; @property (nonatomic) ASVideoNode *videoNode;
@end @end
@@ -51,6 +51,8 @@
{ {
ASVideoNode *nicCageVideo = [[ASVideoNode alloc] init]; 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.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); 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; return YES;
} }
- (void)videoDidReachEnd:(ASVideoNode *)videoNode
{
//Do something with your video if you so desire.
}
@end @end