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
};
@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<ASVideoNodeDelegate> delegate;
- (void)play;
- (void)pause;
- (BOOL)isPlaying;
@end
@protocol ASVideoNodeDelegate <NSObject>
@optional
- (void)videoDidReachEnd:(ASVideoNode *)videoNode;
@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;
__weak id<ASVideoNodeDataSource> _dataSource;
__weak id<ASVideoNodeDelegate> _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];

View File

@@ -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

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"
@interface ViewController()
@interface ViewController()<ASVideoNodeDelegate>
@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