Merge pull request #1549 from ejensen/video-fixes-rebase

[ASVideoNode] Several small bug fixes, improved code quality, added more tests.
This commit is contained in:
appleguy
2016-04-18 21:56:57 -07:00
3 changed files with 208 additions and 69 deletions

View File

@@ -11,6 +11,8 @@
@class AVAsset, AVPlayer, AVPlayerItem;
@protocol ASVideoNodeDelegate;
NS_ASSUME_NONNULL_BEGIN
// IMPORTANT NOTES:
// 1. Applications using ASVideoNode must link AVFoundation! (this provides the AV* classes below)
// 2. This is a relatively new component of AsyncDisplayKit. It has many useful features, but
@@ -19,34 +21,47 @@
@interface ASVideoNode : ASControlNode
- (instancetype)init; // ASVideoNode is created with a simple alloc/init.
- (void)play;
- (void)pause;
- (BOOL)isPlaying;
@property (atomic, strong, readwrite) AVAsset *asset;
@property (nullable, atomic, strong, readwrite) AVAsset *asset;
@property (atomic, strong, readonly) AVPlayer *player;
@property (atomic, strong, readonly) AVPlayerItem *currentItem;
@property (nullable, atomic, strong, readonly) AVPlayer *player;
@property (nullable, atomic, strong, readonly) AVPlayerItem *currentItem;
// When autoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState.
// If it leaves the visible interfaceState it will pause but will resume once it has returned
/**
* When shouldAutoplay is set to true, a video node will play when it has both loaded and entered the "visible" interfaceState.
* If it leaves the visible interfaceState it will pause but will resume once it has returned.
*/
@property (nonatomic, assign, readwrite) BOOL shouldAutoplay;
@property (nonatomic, assign, readwrite) BOOL shouldAutorepeat;
@property (nonatomic, assign, readwrite) BOOL muted;
//! Defaults to AVLayerVideoGravityResizeAspect
@property (atomic) NSString *gravity;
@property (atomic) ASButtonNode *playButton;
@property (atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate;
//! Defaults to an ASDefaultPlayButton instance.
@property (nullable, atomic) ASButtonNode *playButton;
@property (nullable, atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate;
@end
@protocol ASVideoNodeDelegate <NSObject>
@optional
/**
* @abstract Delegate method invoked when the node's video has played to its end time.
* @param videoNode The video node has played to its end time.
*/
- (void)videoPlaybackDidFinish:(ASVideoNode *)videoNode;
/**
* @abstract Delegate method invoked the node is tapped.
* @param videoNode The video node that was tapped.
* @discussion The video's play state is toggled if this method is not implemented.
*/
- (void)videoNodeWasTapped:(ASVideoNode *)videoNode;
@end
NS_ASSUME_NONNULL_END

View File

@@ -9,6 +9,23 @@
#import "ASVideoNode.h"
#import "ASDefaultPlayButton.h"
static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) {
return ASObjectIsEqual(asset1, asset2)
|| ([asset1 isKindOfClass:[AVURLAsset class]]
&& [asset2 isKindOfClass:[AVURLAsset class]]
&& ASObjectIsEqual(((AVURLAsset *)asset1).URL, ((AVURLAsset *)asset2).URL));
}
static UIViewContentMode ASContentModeFromVideoGravity(NSString *videoGravity) {
if ([videoGravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
return UIViewContentModeScaleAspectFit;
} else if ([videoGravity isEqual:AVLayerVideoGravityResizeAspectFill]) {
return UIViewContentModeScaleAspectFill;
} else {
return UIViewContentModeScaleToFill;
}
}
@interface ASVideoNode ()
{
ASDN::RecursiveMutex _videoLock;
@@ -96,6 +113,8 @@
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:_currentPlayerItem];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:_currentPlayerItem];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemNewErrorLogEntryNotification object:_currentPlayerItem];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
}
}
@@ -107,6 +126,8 @@
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
}
}
@@ -153,43 +174,39 @@
imageGenerator.appliesPreferredTrackTransform = YES;
NSArray *times = @[[NSValue valueWithCMTime:CMTimeMake(0, 1)]];
[imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
[imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
ASDN::MutexLocker l(_videoLock);
// Unfortunately it's not possible to generate a preview image for an HTTP live stream asset, so we'll give up here
// http://stackoverflow.com/questions/32112205/m3u8-file-avassetimagegenerator-error
if (image && _placeholderImageNode.image == nil) {
UIImage *theImage = [UIImage imageWithCGImage:image];
if (!_placeholderImageNode) {
_placeholderImageNode = [[ASImageNode alloc] init];
_placeholderImageNode.layerBacked = YES;
}
_placeholderImageNode.image = theImage;
if ([_gravity isEqualToString:AVLayerVideoGravityResize]) {
_placeholderImageNode.contentMode = UIViewContentModeRedraw;
}
else if ([_gravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
_placeholderImageNode.contentMode = UIViewContentModeScaleAspectFit;
}
else if ([_gravity isEqualToString:AVLayerVideoGravityResizeAspectFill]) {
_placeholderImageNode.contentMode = UIViewContentModeScaleAspectFill;
}
dispatch_async(dispatch_get_main_queue(), ^{
ASDN::MutexLocker l(_videoLock);
[self insertSubnode:_placeholderImageNode atIndex:0];
[self setNeedsLayout];
});
[self setPlaceholderImage:[UIImage imageWithCGImage:image]];
}
}];
});
}
- (void)setPlaceholderImage:(UIImage *)image
{
ASDN::MutexLocker l(_videoLock);
if (_placeholderImageNode == nil) {
_placeholderImageNode = [[ASImageNode alloc] init];
_placeholderImageNode.layerBacked = YES;
_placeholderImageNode.contentMode = ASContentModeFromVideoGravity(_gravity);
}
_placeholderImageNode.image = image;
dispatch_async(dispatch_get_main_queue(), ^{
ASDN::MutexLocker l(_videoLock);
[self insertSubnode:_placeholderImageNode atIndex:0];
[self setNeedsLayout];
});
}
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
{
[super interfaceStateDidChange:newState fromState:oldState];
@@ -326,7 +343,10 @@
- (void)setPlayButton:(ASButtonNode *)playButton
{
ASDN::MutexLocker l(_videoLock);
[_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
[_playButton removeFromSupernode];
_playButton = playButton;
[self addSubnode:playButton];
@@ -345,13 +365,10 @@
{
ASDN::MutexLocker l(_videoLock);
if (ASObjectIsEqual(asset, _asset)
|| ([asset isKindOfClass:[AVURLAsset class]]
&& [_asset isKindOfClass:[AVURLAsset class]]
&& ASObjectIsEqual(((AVURLAsset *)asset).URL, ((AVURLAsset *)_asset).URL))) {
return;
}
if (ASAssetIsEqual(asset, _asset)) {
return;
}
_asset = asset;
// FIXME: Adopt -setNeedsFetchData when it is available
@@ -378,6 +395,7 @@
if (_playerNode.isNodeLoaded) {
((AVPlayerLayer *)_playerNode.layer).videoGravity = gravity;
}
_placeholderImageNode.contentMode = ASContentModeFromVideoGravity(gravity);
_gravity = gravity;
}
@@ -473,8 +491,8 @@
if ([_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]) {
[_delegate videoPlaybackDidFinish:self];
}
[_player seekToTime:CMTimeMakeWithSeconds(0, 1)];
[_player seekToTime:kCMTimeZero];
if (_shouldAutorepeat) {
[self play];
} else {
@@ -486,19 +504,37 @@
{
if ([notification.name isEqualToString:AVPlayerItemFailedToPlayToEndTimeNotification]) {
NSLog(@"Failed to play video");
}
else if ([notification.name isEqualToString:AVPlayerItemNewErrorLogEntryNotification]) {
AVPlayerItem* item = (AVPlayerItem*)notification.object;
AVPlayerItemErrorLogEvent* logEvent = item.errorLog.events.lastObject;
} else if ([notification.name isEqualToString:AVPlayerItemNewErrorLogEntryNotification]) {
AVPlayerItem *item = (AVPlayerItem *)notification.object;
AVPlayerItemErrorLogEvent *logEvent = item.errorLog.events.lastObject;
NSLog(@"AVPlayerItem error log entry added for video with error %@ status %@", item.error,
(item.status == AVPlayerItemStatusFailed ? @"FAILED" : [NSString stringWithFormat:@"%ld", (long)item.status]));
NSLog(@"Item is %@", item);
if (logEvent)
if (logEvent) {
NSLog(@"Log code %ld domain %@ comment %@", (long)logEvent.errorStatusCode, logEvent.errorDomain, logEvent.errorComment);
}
}
}
- (void)willEnterForeground:(NSNotification *)notification
{
ASDN::MutexLocker l(_videoLock);
if (_shouldBePlaying) {
[self play];
}
}
- (void)didEnterBackground:(NSNotification *)notification
{
ASDN::MutexLocker l(_videoLock);
if (_shouldBePlaying) {
[self pause];
_shouldBePlaying = YES;
}
}
#pragma mark - Property Accessors for Tests
@@ -508,6 +544,12 @@
return _spinner;
}
- (ASImageNode *)placeholderImageNode
{
ASDN::MutexLocker l(_videoLock);
return _placeholderImageNode;
}
- (AVPlayerItem *)currentItem
{
ASDN::MutexLocker l(_videoLock);
@@ -526,6 +568,18 @@
return _playerNode;
}
- (void)setPlayerNode:(ASDisplayNode *)playerNode
{
ASDN::MutexLocker l(_videoLock);
_playerNode = playerNode;
}
- (void)setPlayer:(AVPlayer *)player
{
ASDN::MutexLocker l(_videoLock);
_player = player;
}
- (BOOL)shouldBePlaying
{
ASDN::MutexLocker l(_videoLock);
@@ -536,6 +590,7 @@
- (void)dealloc
{
[_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
[self removePlayerItemObservers];
@try {

View File

@@ -24,25 +24,14 @@
ASDisplayNode *_playerNode;
AVPlayer *_player;
}
@property (atomic) ASInterfaceState interfaceState;
@property (atomic) ASDisplayNode *spinner;
@property (atomic) ASDisplayNode *playerNode;
@property (atomic) BOOL shouldBePlaying;
@property (atomic, readwrite) ASInterfaceState interfaceState;
@property (atomic, readonly) ASDisplayNode *spinner;
@property (atomic, readonly) ASImageNode *placeholderImageNode;
@property (atomic, readwrite) ASDisplayNode *playerNode;
@property (atomic, readwrite) AVPlayer *player;
@property (atomic, readonly) BOOL shouldBePlaying;
- (void)setPlayerNode:(ASDisplayNode *)playerNode;
@end
@implementation ASVideoNode (Test)
- (void)setPlayerNode:(ASDisplayNode *)playerNode
{
_playerNode = playerNode;
}
- (void)setPlayer:(AVPlayer *)player
{
_player = player;
}
- (void)setPlaceholderImage:(UIImage *)image;
@end
@@ -124,7 +113,7 @@
_videoNode.interfaceState = ASInterfaceStateFetchData;
[_videoNode play];
[_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL];
[_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{NSKeyValueChangeNewKey : @(AVPlayerItemStatusReadyToPlay)} context:NULL];
XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating);
}
@@ -293,4 +282,84 @@
XCTAssertFalse(_videoNode.player.muted);
}
- (void)testVideoThatDoesNotAutorepeatsShouldPauseOnPlaybackEnd
{
_videoNode.asset = _firstAsset;
_videoNode.shouldAutorepeat = NO;
[_videoNode didLoad];
[_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData];
[_videoNode play];
XCTAssertTrue(_videoNode.isPlaying);
[[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem];
XCTAssertFalse(_videoNode.isPlaying);
XCTAssertEqual(0, CMTimeGetSeconds(_videoNode.player.currentTime));
}
- (void)testVideoThatAutorepeatsShouldRepeatOnPlaybackEnd
{
_videoNode.asset = _firstAsset;
_videoNode.shouldAutorepeat = YES;
[_videoNode didLoad];
[_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData];
[_videoNode play];
[[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem];
XCTAssertTrue(_videoNode.isPlaying);
}
- (void)testBackgroundingAndForegroungingTheAppShouldPauseAndResume
{
_videoNode.asset = _firstAsset;
[_videoNode didLoad];
[_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData];
[_videoNode play];
XCTAssertTrue(_videoNode.isPlaying);
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationDidEnterBackgroundNotification object:nil];
XCTAssertFalse(_videoNode.isPlaying);
[[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillEnterForegroundNotification object:nil];
XCTAssertTrue(_videoNode.isPlaying);
}
- (void)testSettingVideoGravityChangesPlaceholderContentMode
{
[_videoNode setPlaceholderImage:[[UIImage alloc] init]];
XCTAssertEqual(UIViewContentModeScaleAspectFit, _videoNode.placeholderImageNode.contentMode);
_videoNode.gravity = AVLayerVideoGravityResize;
XCTAssertEqual(UIViewContentModeScaleToFill, _videoNode.placeholderImageNode.contentMode);
_videoNode.gravity = AVLayerVideoGravityResizeAspect;
XCTAssertEqual(UIViewContentModeScaleAspectFit, _videoNode.placeholderImageNode.contentMode);
_videoNode.gravity = AVLayerVideoGravityResizeAspectFill;
XCTAssertEqual(UIViewContentModeScaleAspectFill, _videoNode.placeholderImageNode.contentMode);
}
- (void)testChangingPlayButtonPerformsProperCleanup
{
ASButtonNode *firstButton = _videoNode.playButton;
XCTAssertTrue([firstButton.allTargets containsObject:_videoNode]);
ASButtonNode *secondButton = [[ASButtonNode alloc] init];
_videoNode.playButton = secondButton;
XCTAssertTrue([secondButton.allTargets containsObject:_videoNode]);
XCTAssertEqual(_videoNode, secondButton.supernode);
XCTAssertFalse([firstButton.allTargets containsObject:_videoNode]);
XCTAssertNotEqual(_videoNode, firstButton.supernode);
}
@end