mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-04-22 19:13:55 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user