diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0605feef --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[**.{h,cc,mm,m}] +indent_style = space +indent_size = 2 + +[*.{md,markdown}] +trim_trailing_whitespace = false + +# Makefiles always use tabs for indentation +[Makefile] +indent_style = tab \ No newline at end of file diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 72eaa2a9..cc4eff21 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |spec| spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com', 'Ryan Nystrom' => 'rnystrom@fb.com' } spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' - spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.9.7.3' } + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.9.73' } spec.documentation_url = 'http://asyncdisplaykit.org/appledoc/' @@ -69,5 +69,6 @@ Pod::Spec.new do |spec| } spec.ios.deployment_target = '7.0' - spec.tvos.deployment_target = '9.0' +# Uncomment when fixed: The platform of the target `Pods` (tvOS 9.0) is not compatible with `PINRemoteImage/iOS (2.1.4)`, which does not support `tvos`.) during validation +# spec.tvos.deployment_target = '9.0' end diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index fa42a3c3..27a677fe 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -122,10 +122,14 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _URL = URL; - if (reset || _URL == nil) { + BOOL hasURL = _URL == nil; + if (reset || hasURL) { self.image = _defaultImage; - ASPerformBlockOnMainThread(^{ - self.currentImageQuality = 1.0; + /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, + so always use a dispatch_async to ensure that we queue the operations in the correct order. + (see comment in displayDidFinish) */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.currentImageQuality = hasURL ? 0.0 : 1.0; }); } @@ -151,8 +155,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _defaultImage = defaultImage; if (!_imageLoaded) { - ASPerformBlockOnMainThread(^{ - self.currentImageQuality = 0.0; + BOOL hasURL = _URL == nil; + /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, + so always use a dispatch_async to ensure that we queue the operations in the correct order. + (see comment in displayDidFinish) */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.currentImageQuality = hasURL ? 0.0 : 1.0; }); _lock.unlock(); // Locking: it is important to release _lock before entering setImage:, as it needs to release the lock before -invalidateCalculatedLayout. @@ -229,7 +237,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (result) { self.image = result; _imageLoaded = YES; - _currentImageQuality = 1.0; + dispatch_async(dispatch_get_main_queue(), ^{ + _currentImageQuality = 1.0; + }); } } } @@ -322,7 +332,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return; } strongSelf.image = progressImage; - ASPerformBlockOnMainThread(^{ + dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = progress; }); }; @@ -347,7 +357,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; self.animatedImage = nil; self.image = _defaultImage; _imageLoaded = NO; - ASPerformBlockOnMainThread(^{ + dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = 0.0; }); } @@ -431,9 +441,14 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } } } - + _imageLoaded = YES; - self.currentImageQuality = 1.0; + /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, + so always use a dispatch_async to ensure that we queue the operations in the correct order. + (see comment in displayDidFinish) */ + dispatch_async(dispatch_get_main_queue(), ^{ + self.currentImageQuality = 1.0; + }); [_delegate imageNode:self didLoadImage:self.image]; }); } @@ -459,7 +474,9 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } else { strongSelf.image = [imageContainer asdk_image]; } - strongSelf->_currentImageQuality = 1.0; + dispatch_async(dispatch_get_main_queue(), ^{ + strongSelf->_currentImageQuality = 1.0; + }); } strongSelf->_downloadIdentifier = nil; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index e8fff078..d925f3db 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -101,7 +101,7 @@ static NSString * const kStatus = @"status"; - (ASDisplayNode *)constructPlayerNode { ASVideoNode * __weak weakSelf = self; - + return [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; playerLayer.player = weakSelf.player; @@ -113,16 +113,47 @@ static NSString * const kStatus = @"status"; - (AVPlayerItem *)constructPlayerItem { ASDN::MutexLocker l(_videoLock); - + if (_asset != nil) { - if ([_asset isKindOfClass:[AVURLAsset class]]) { - return [[AVPlayerItem alloc] initWithURL:((AVURLAsset *)_asset).URL]; - } else { - return [[AVPlayerItem alloc] initWithAsset:_asset]; + return [[AVPlayerItem alloc] initWithAsset:_asset]; + } + + return nil; +} + +- (void)prepareToPlayAsset:(AVAsset *)asset withKeys:(NSArray *)requestedKeys +{ + for (NSString *key in requestedKeys) { + NSError *error = nil; + AVKeyValueStatus keyStatus = [asset statusOfValueForKey:key error:&error]; + if (keyStatus == AVKeyValueStatusFailed) { + NSLog(@"Asset loading failed with error: %@", error); } } - - return nil; + + if (![asset isPlayable]) { + NSLog(@"Asset is not playable."); + return; + } + + AVPlayerItem *playerItem = [self constructPlayerItem]; + [self setCurrentItem:playerItem]; + + if (_player != nil) { + [_player replaceCurrentItemWithPlayerItem:playerItem]; + } else { + self.player = [AVPlayer playerWithPlayerItem:playerItem]; + } + + if (_placeholderImageNode.image == nil) { + [self generatePlaceholderImage]; + } + + __weak __typeof(self) weakSelf = self; + _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); + _timeObserver = [_player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ + [weakSelf periodicTimeObserver:time]; + }]; } - (void)addPlayerItemObservers:(AVPlayerItem *)playerItem @@ -130,7 +161,7 @@ static NSString * const kStatus = @"status"; [playerItem addObserver:self forKeyPath:kStatus options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:ASVideoNodeContext]; [playerItem addObserver:self forKeyPath:kPlaybackLikelyToKeepUpKey options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; [playerItem addObserver:self forKeyPath:kplaybackBufferEmpty options:NSKeyValueObservingOptionNew context:ASVideoNodeContext]; - + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; [notificationCenter addObserver:self selector:@selector(errorWhilePlaying:) name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; @@ -147,7 +178,7 @@ static NSString * const kStatus = @"status"; @catch (NSException * __unused exception) { NSLog(@"Unnecessary KVO removal"); } - + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem]; [notificationCenter removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:playerItem]; @@ -178,7 +209,7 @@ static NSString * const kStatus = @"status"; { ASVideoNode * __weak weakSelf = self; AVAsset * __weak asset = self.asset; - + [self imageAtTime:kCMTimeZero completionHandler:^(UIImage *image) { ASPerformBlockOnMainThread(^{ // Ensure the asset hasn't changed since the image request was made @@ -193,17 +224,17 @@ static NSString * const kStatus = @"status"; { ASPerformBlockOnBackgroundThread(^{ AVAsset *asset = self.asset; - + // Skip the asset image generation if we don't have any tracks available that are capable of supporting it NSArray* visualAssetArray = [asset tracksWithMediaCharacteristic:AVMediaCharacteristicVisual]; if (visualAssetArray.count == 0) { completionHandler(nil); return; } - + AVAssetImageGenerator *previewImageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset]; previewImageGenerator.appliesPreferredTrackTransform = YES; - + [previewImageGenerator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:imageTime]] completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) { if (error != nil && result != AVAssetImageGeneratorCancelled) { @@ -217,18 +248,18 @@ static NSString * const kStatus = @"status"; - (void)setVideoPlaceholderImage:(UIImage *)image { ASDN::MutexLocker l(_videoLock); - + if (_placeholderImageNode == nil && image != nil) { _placeholderImageNode = [[ASImageNode alloc] init]; _placeholderImageNode.layerBacked = YES; _placeholderImageNode.contentMode = ASContentModeFromVideoGravity(_gravity); } - + _placeholderImageNode.image = image; - + ASPerformBlockOnMainThread(^{ ASDN::MutexLocker l(_videoLock); - + if (_placeholderImageNode != nil) { [self insertSubnode:_placeholderImageNode atIndex:0]; [self setNeedsLayout]; @@ -239,11 +270,11 @@ static NSString * const kStatus = @"status"; - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { ASDN::MutexLocker l(_videoLock); - + if (object != _currentPlayerItem) { return; } - + if ([keyPath isEqualToString:kStatus]) { if ([change[NSKeyValueChangeNewKey] integerValue] == AVPlayerItemStatusReadyToPlay) { [self removeSpinner]; @@ -285,26 +316,14 @@ static NSString * const kStatus = @"status"; [super fetchData]; { - ASDN::MutexLocker l(_videoLock); - - AVPlayerItem *playerItem = [self constructPlayerItem]; - self.currentItem = playerItem; - - if (_player != nil) { - [_player replaceCurrentItemWithPlayerItem:playerItem]; - } else { - self.player = [AVPlayer playerWithPlayerItem:playerItem]; - } - - if (_placeholderImageNode.image == nil) { - [self generatePlaceholderImage]; - } - - __weak __typeof(self) weakSelf = self; - _timeObserverInterval = CMTimeMake(1, _periodicTimeObserverTimescale); - _timeObserver = [_player addPeriodicTimeObserverForInterval:_timeObserverInterval queue:NULL usingBlock:^(CMTime time){ - [weakSelf periodicTimeObserver:time]; - }]; + ASDN::MutexLocker l(_videoLock); + AVAsset *asset = self.asset; + NSArray *requestedKeys = @[ @"playable" ]; + [asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler:^{ + ASPerformBlockOnMainThread(^{ + [self prepareToPlayAsset:asset withKeys:requestedKeys]; + }); + }]; } } @@ -325,11 +344,11 @@ static NSString * const kStatus = @"status"; [super clearFetchedData]; { - ASDN::MutexLocker l(_videoLock); - - self.player = nil; - self.currentItem = nil; - _placeholderImageNode.image = nil; + ASDN::MutexLocker l(_videoLock); + + self.player = nil; + self.currentItem = nil; + _placeholderImageNode.image = nil; } } @@ -372,10 +391,10 @@ static NSString * const kStatus = @"status"; - (void)setPlayButton:(ASButtonNode *)playButton { ASDN::MutexLocker l(_videoLock); - + [_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; [_playButton removeFromSupernode]; - + _playButton = playButton; [self addSubnode:playButton]; @@ -397,13 +416,13 @@ static NSString * const kStatus = @"status"; if (ASAssetIsEqual(asset, _asset)) { return; } - + [self clearFetchedData]; - + _asset = asset; - + [self setNeedsDataFetch]; - + if (_shouldAutoplay) { [self play]; } @@ -480,14 +499,14 @@ static NSString * const kStatus = @"status"; if(![self isStateChangeValid:ASVideoNodePlayerStatePlaying]){ return; } - + if (_player == nil) { [self setNeedsDataFetch]; } - + if (_playerNode == nil) { _playerNode = [self constructPlayerNode]; - + if (_playButton.supernode == self) { [self insertSubnode:_playerNode belowSubnode:_playButton]; } else { @@ -585,7 +604,7 @@ static NSString * const kStatus = @"status"; [_delegate videoPlaybackDidFinish:self]; } [_player seekToTime:kCMTimeZero]; - + if (_shouldAutorepeat) { [self play]; } else { @@ -633,11 +652,11 @@ static NSString * const kStatus = @"status"; - (void)setCurrentItem:(AVPlayerItem *)currentItem { ASDN::MutexLocker l(_videoLock); - + [self removePlayerItemObservers:_currentPlayerItem]; - + _currentPlayerItem = currentItem; - + [self addPlayerItemObservers:currentItem]; } diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index 3effa243..6530edd4 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -6,6 +6,8 @@ * of patent rights can be found in the PATENTS file in the same directory. */ +#import + #import #import #import @@ -17,6 +19,7 @@ AVURLAsset *_firstAsset; AVAsset *_secondAsset; NSURL *_url; + NSArray *_requestedKeys; } @end @@ -32,6 +35,7 @@ @property (atomic, readwrite) BOOL shouldBePlaying; - (void)setVideoPlaceholderImage:(UIImage *)image; +- (void)prepareToPlayAsset:(AVAsset *)asset withKeys:(NSArray *)requestedKeys; @end @@ -43,6 +47,7 @@ _firstAsset = [AVURLAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; _secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; _url = [NSURL URLWithString:@"testURL"]; + _requestedKeys = @[ @"playable" ]; } @@ -131,23 +136,42 @@ XCTAssertNil(_videoNode.player); } -- (void)testPlayerIsCreatedInFetchData +- (void)testPlayerIsCreatedAsynchronouslyInFetchData { - _videoNode.asset = _firstAsset; + AVAsset *asset = _firstAsset; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; _videoNode.interfaceState = ASInterfaceStateFetchData; + [videoNodeMock verifyWithDelay:1.0f]; + XCTAssertNotNil(_videoNode.player); } -- (void)testPlayerIsCreatedInFetchDataWithURL +- (void)testPlayerIsCreatedAsynchronouslyInFetchDataWithURL { - _videoNode.asset = [AVAsset assetWithURL:_url]; + AVAsset *asset = [AVAsset assetWithURL:_url]; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; _videoNode.interfaceState = ASInterfaceStateFetchData; + [videoNodeMock verifyWithDelay:1.0f]; + XCTAssertNotNil(_videoNode.player); } - - (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlaying { _videoNode.asset = _firstAsset; @@ -284,13 +308,17 @@ - (void)testVideoThatDoesNotAutorepeatsShouldPauseOnPlaybackEnd { - _videoNode.asset = _firstAsset; + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; _videoNode.shouldAutorepeat = NO; [_videoNode didLoad]; [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; [_videoNode play]; - + XCTAssertTrue(_videoNode.isPlaying); [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; @@ -301,11 +329,15 @@ - (void)testVideoThatAutorepeatsShouldRepeatOnPlaybackEnd { - _videoNode.asset = _firstAsset; + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; _videoNode.shouldAutorepeat = YES; [_videoNode didLoad]; [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; [_videoNode play]; [[NSNotificationCenter defaultCenter] postNotificationName:AVPlayerItemDidPlayToEndTimeNotification object:_videoNode.currentItem]; @@ -315,9 +347,13 @@ - (void)testVideoResumedWhenBufferIsLikelyToKeepUp { - _videoNode.asset = _firstAsset; + id assetMock = [OCMockObject partialMockForObject:_firstAsset]; + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + + _videoNode.asset = assetMock; [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay | ASInterfaceStateFetchData]; + [_videoNode prepareToPlayAsset:assetMock withKeys:_requestedKeys]; [_videoNode pause]; _videoNode.shouldBePlaying = YES; @@ -372,9 +408,20 @@ - (void)testClearingFetchedContentShouldClearAssetData { - _videoNode.asset = _firstAsset; + AVAsset *asset = _firstAsset; + + id assetMock = [OCMockObject partialMockForObject:asset]; + id videoNodeMock = [OCMockObject partialMockForObject:_videoNode]; + + [[[assetMock stub] andReturnValue:@YES] isPlayable]; + [[[videoNodeMock expect] andForwardToRealObject] prepareToPlayAsset:assetMock withKeys:_requestedKeys]; + + _videoNode.asset = assetMock; [_videoNode fetchData]; [_videoNode setVideoPlaceholderImage:[[UIImage alloc] init]]; + + [videoNodeMock verifyWithDelay:1.0f]; + XCTAssertNotNil(_videoNode.player); XCTAssertNotNil(_videoNode.currentItem); XCTAssertNotNil(_videoNode.placeholderImageNode.image);