diff --git a/AsyncDisplayKit/ASVideoNode.h b/AsyncDisplayKit/ASVideoNode.h index 99768eb82c..1615ce7cdb 100644 --- a/AsyncDisplayKit/ASVideoNode.h +++ b/AsyncDisplayKit/ASVideoNode.h @@ -18,7 +18,10 @@ // in an issue on GitHub: https://github.com/facebook/AsyncDisplayKit/issues @interface ASVideoNode : ASControlNode + +@property (atomic, strong, readwrite) NSURL *url; @property (atomic, strong, readwrite) AVAsset *asset; + @property (atomic, strong, readonly) AVPlayer *player; @property (atomic, strong, readonly) AVPlayerItem *currentItem; @@ -34,6 +37,8 @@ @property (atomic, weak, readwrite) id delegate; +- (instancetype)init; + - (void)play; - (void)pause; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index d0c65e6ae6..17ad3cb117 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -14,17 +14,18 @@ ASDN::RecursiveMutex _videoLock; __weak id _delegate; - + BOOL _shouldBePlaying; BOOL _shouldAutorepeat; BOOL _shouldAutoplay; BOOL _muted; - - AVAsset *_asset; - AVPlayerItem *_currentItem; + AVAsset *_asset; + NSURL *_url; + + AVPlayerItem *_currentPlayerItem; AVPlayer *_player; ASImageNode *_placeholderImageNode; @@ -41,70 +42,81 @@ @implementation ASVideoNode +//TODO: Have a bash at supplying a preview image node for use with HLS videos as we can't have a priview with those + + +#pragma mark - Construction and Layout + - (instancetype)init { - if (!(self = [super init])) { - return nil; - } - - _previewQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); + _previewQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); self.playButton = [[ASDefaultPlayButton alloc] init]; - self.gravity = AVLayerVideoGravityResizeAspect; - [self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; - + return self; } -- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock { - [super interfaceStateDidChange:newState fromState:oldState]; + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (ASDisplayNode*)constructPlayerNode +{ + ASDisplayNode* playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ + AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; + if (!_player) { + [self constructCurrentPlayerItemFromInitData]; + _player = [AVPlayer playerWithPlayerItem:_currentPlayerItem]; + _player.muted = _muted; + } + playerLayer.player = _player; + playerLayer.videoGravity = [self gravity]; + return playerLayer; + }]; - if (!(newState & ASInterfaceStateVisible)) { - if (oldState & ASInterfaceStateVisible) { - if (_shouldBePlaying) { - [self pause]; - _shouldBePlaying = YES; - } - [(UIActivityIndicatorView *)_spinner.view stopAnimating]; - [_spinner removeFromSupernode]; - } - } else { - if (_shouldBePlaying) { - [self play]; - } + return playerNode; +} + +- (void)constructCurrentPlayerItemFromInitData +{ + ASDisplayNodeAssert(_asset || _url, @"ASVideoNode must be initialised with either an AVAsset or URL"); + [self removePlayerItemObservers]; + + if (_asset) { + _currentPlayerItem = [[AVPlayerItem alloc] initWithAsset:_asset]; + } else if (_url) { + _currentPlayerItem = [[AVPlayerItem alloc] initWithURL:_url]; + } + + if (_currentPlayerItem) { + [[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]; } } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +- (void)removePlayerItemObservers { - if ([change[@"new"] integerValue] == AVPlayerItemStatusReadyToPlay) { - if ([self.subnodes containsObject:_spinner]) { - [_spinner removeFromSupernode]; - _spinner = nil; - } - } - - if ([change[@"new"] integerValue] == AVPlayerItemStatusFailed) { - + if (_currentPlayerItem) { + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemFailedToPlayToEndTimeNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemNewErrorLogEntryNotification object:nil]; } } -- (void)didPlayToEnd:(NSNotification *)notification +- (void)didLoad { - if (ASObjectIsEqual([[notification object] asset], _asset)) { - if ([_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]) { - [_delegate videoPlaybackDidFinish:self]; - } - [_player seekToTime:CMTimeMakeWithSeconds(0, 1)]; - - if (_shouldAutorepeat) { - [self play]; - } else { - [self pause]; - } + [super didLoad]; + + if (_shouldBePlaying) { + _playerNode = [self constructPlayerNode]; + [self insertSubnode:_playerNode atIndex:0]; + } else if (_asset) { + [self setPlaceholderImagefromAsset:_asset]; } } @@ -128,29 +140,22 @@ _spinner.position = CGPointMake(bounds.size.width/2, bounds.size.height/2); } -- (void)didLoad +- (void)setPlaceholderImagefromAsset:(AVAsset*)asset { - [super didLoad]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; - - if (_shouldBePlaying) { - _playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ - AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; - if (!_player) { - _player = [AVPlayer playerWithPlayerItem:[[AVPlayerItem alloc] initWithAsset:_asset]]; - _player.muted = _muted; - } - playerLayer.player = _player; - playerLayer.videoGravity = [self gravity]; - return playerLayer; - }]; + ASDN::MutexLocker l(_videoLock); + if (!_placeholderImageNode) + _placeholderImageNode = [[ASImageNode alloc] init]; + + dispatch_async(_previewQueue, ^{ + AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset]; + imageGenerator.appliesPreferredTrackTransform = YES; + NSArray *times = @[[NSValue valueWithCMTime:CMTimeMake(0, 1)]]; - [self insertSubnode:_playerNode atIndex:0]; - } else { - dispatch_async(_previewQueue, ^{ - AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset]; - imageGenerator.appliesPreferredTrackTransform = YES; - [imageGenerator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:CMTimeMake(0, 1)]] 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) { + + // 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]; _placeholderImageNode = [[ASImageNode alloc] init]; @@ -171,8 +176,58 @@ _placeholderImageNode.frame = self.bounds; [self insertSubnode:_placeholderImageNode atIndex:0]; }); - }]; - }); + } + }]; + }); +} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + + BOOL nowVisible = ASInterfaceStateIncludesVisible(newState); + BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); + + if (!nowVisible) { + if (wasVisible) { + if (_shouldBePlaying) { + [self pause]; + _shouldBePlaying = YES; + } + [(UIActivityIndicatorView *)_spinner.view stopAnimating]; + [_spinner removeFromSupernode]; + } + } else { + if (_shouldBePlaying) { + [self play]; + } + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (object == _currentPlayerItem && [keyPath isEqualToString:@"status"]) { + if (_currentPlayerItem.status == AVPlayerItemStatusReadyToPlay) { + if ([self.subnodes containsObject:_spinner]) { + [_spinner removeFromSupernode]; + _spinner = nil; + } + + // If we don't yet have a placeholder image update it now that we should have data available for it + if (!_placeholderImageNode) { + if (_currentPlayerItem && + _currentPlayerItem.tracks.count > 0 && + _currentPlayerItem.tracks[0].assetTrack && + _currentPlayerItem.tracks[0].assetTrack.asset) { + _asset = _currentPlayerItem.tracks[0].assetTrack.asset; + [self setPlaceholderImagefromAsset:_asset]; + [self setNeedsLayout]; + } + } + + } else if (_currentPlayerItem.status == AVPlayerItemStatusFailed) { + + } } } @@ -189,32 +244,26 @@ } } -- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock -{ - ASDisplayNodeAssertNotSupported(); - return nil; -} - - (void)fetchData { [super fetchData]; - + @try { - [_currentItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; + [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; } @catch (NSException * __unused exception) { NSLog(@"unnecessary removal in fetch data"); } - + { ASDN::MutexLocker l(_videoLock); - _currentItem = [[AVPlayerItem alloc] initWithAsset:_asset]; - [_currentItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; - + [self constructCurrentPlayerItemFromInitData]; + [_currentPlayerItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; + if (_player) { - [_player replaceCurrentItemWithPlayerItem:_currentItem]; + [_player replaceCurrentItemWithPlayerItem:_currentPlayerItem]; } else { - _player = [[AVPlayer alloc] initWithPlayerItem:_currentItem]; + _player = [[AVPlayer alloc] initWithPlayerItem:_currentPlayerItem]; _player.muted = _muted; } } @@ -245,18 +294,20 @@ if (isVisible) { if (_playerNode.isNodeLoaded) { if (!_player) { - _player = [AVPlayer playerWithPlayerItem:[[AVPlayerItem alloc] initWithAsset:_asset]]; + [self constructCurrentPlayerItemFromInitData]; + _player = [AVPlayer playerWithPlayerItem:_currentPlayerItem]; _player.muted = _muted; } ((AVPlayerLayer *)_playerNode.layer).player = _player; } - + if (_shouldBePlaying) { [self play]; } } } + #pragma mark - Video Properties - (void)setPlayButton:(ASButtonNode *)playButton @@ -282,11 +333,11 @@ ASDN::MutexLocker l(_videoLock); if (ASObjectIsEqual(asset, _asset) - || ([asset isKindOfClass:[AVURLAsset class]] - && [_asset isKindOfClass:[AVURLAsset class]] - && ASObjectIsEqual(((AVURLAsset *)asset).URL, ((AVURLAsset *)_asset).URL))) { - return; - } + || ([asset isKindOfClass:[AVURLAsset class]] + && [_asset isKindOfClass:[AVURLAsset class]] + && ASObjectIsEqual(((AVURLAsset *)asset).URL, ((AVURLAsset *)_asset).URL))) { + return; + } _asset = asset; @@ -302,6 +353,27 @@ return _asset; } +- (void)setUrl:(NSURL *)url +{ + ASDN::MutexLocker l(_videoLock); + + if (ASObjectIsEqual(url, _url)) + return; + + _url = url; + + // FIXME: Adopt -setNeedsFetchData when it is available + if (self.interfaceState & ASInterfaceStateFetchData) { + [self fetchData]; + } +} + +- (NSURL *)url +{ + ASDN::MutexLocker l(_videoLock); + return _url; +} + - (AVPlayer *)player { ASDN::MutexLocker l(_videoLock); @@ -327,7 +399,7 @@ - (BOOL)muted { ASDN::MutexLocker l(_videoLock); - + return _muted; } @@ -355,16 +427,7 @@ } if (!_playerNode) { - _playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ - AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; - if (!_player) { - _player = [AVPlayer playerWithPlayerItem:[[AVPlayerItem alloc] initWithAsset:_asset]]; - _player.muted = _muted; - } - playerLayer.player = _player; - playerLayer.videoGravity = [self gravity]; - return playerLayer; - }]; + _playerNode = [self constructPlayerNode]; if ([self.subnodes containsObject:_playButton]) { [self insertSubnode:_playerNode belowSubnode:_playButton]; @@ -380,7 +443,7 @@ _playButton.alpha = 0.0; }]; - if (![self ready] && _shouldBePlaying && (self.interfaceState & ASInterfaceStateVisible)) { + if (![self ready] && _shouldBePlaying && ASInterfaceStateIncludesVisible(self.interfaceState)) { [self addSubnode:_spinner]; [(UIActivityIndicatorView *)_spinner.view startAnimating]; } @@ -388,7 +451,7 @@ - (BOOL)ready { - return _currentItem.status == AVPlayerItemStatusReadyToPlay; + return _currentPlayerItem.status == AVPlayerItemStatusReadyToPlay; } - (void)pause @@ -410,6 +473,41 @@ return (_player.rate > 0 && !_player.error); } + +#pragma mark - Playback observers + +- (void)didPlayToEnd:(NSNotification *)notification +{ + if ([_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]) { + [_delegate videoPlaybackDidFinish:self]; + } + [_player seekToTime:CMTimeMakeWithSeconds(0, 1)]; + + if (_shouldAutorepeat) { + [self play]; + } else { + [self pause]; + } +} + +- (void)errorWhilePlaying:(NSNotification *)notification +{ + 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; + 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) + NSLog(@"Log code %ld domain %@ comment %@", (long)logEvent.errorStatusCode, logEvent.errorDomain, logEvent.errorComment); + } +} + + #pragma mark - Property Accessors for Tests - (ASDisplayNode *)spinner @@ -421,13 +519,13 @@ - (AVPlayerItem *)currentItem { ASDN::MutexLocker l(_videoLock); - return _currentItem; + return _currentPlayerItem; } - (void)setCurrentItem:(AVPlayerItem *)currentItem { ASDN::MutexLocker l(_videoLock); - _currentItem = currentItem; + _currentPlayerItem = currentItem; } - (ASDisplayNode *)playerNode @@ -446,9 +544,10 @@ - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; + [self removePlayerItemObservers]; + @try { - [_currentItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; + [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; } @catch (NSException * __unused exception) { NSLog(@"unnecessary removal in dealloc"); diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index adabc9e705..0999918059 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -16,6 +16,7 @@ ASVideoNode *_videoNode; AVURLAsset *_firstAsset; AVAsset *_secondAsset; + NSURL *_url; } @end @@ -52,100 +53,147 @@ _videoNode = [[ASVideoNode alloc] init]; _firstAsset = [AVURLAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; _secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; + _url = [NSURL URLWithString:@"testURL"]; } -- (void)testVideoNodeReplacesAVPlayerItemWhenNewURLIsSet -{ - _videoNode.interfaceState = ASInterfaceStateFetchData; - _videoNode.asset = _firstAsset; - - AVPlayerItem *item = [_videoNode currentItem]; - - _videoNode.asset = _secondAsset; - AVPlayerItem *secondItem = [_videoNode currentItem]; - - XCTAssertNotEqualObjects(item, secondItem); -} - -- (void)testVideoNodeDoesNotReplaceAVPlayerItemWhenSameURLIsSet -{ - _videoNode.interfaceState = ASInterfaceStateFetchData; - - _videoNode.asset = _firstAsset; - AVPlayerItem *item = [_videoNode currentItem]; - - _videoNode.asset = [AVAsset assetWithURL:_firstAsset.URL]; - AVPlayerItem *secondItem = [_videoNode currentItem]; - - XCTAssertEqualObjects(item, secondItem); -} - (void)testSpinnerDefaultsToNil { XCTAssertNil(_videoNode.spinner); } + - (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode { - _videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset; - + [self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl]; +} + +- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl +{ + _videoNode.url = _url; + [self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl]; +} + +- (void)doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl +{ + _videoNode.interfaceState = ASInterfaceStateFetchData; [_videoNode play]; XCTAssertNotNil(_videoNode.spinner); } + - (void)testOnPauseSpinnerIsPausedIfPresent { - _videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset; + [self doOnPauseSpinnerIsPausedIfPresentWithURL]; +} + +- (void)testOnPauseSpinnerIsPausedIfPresentWithURL +{ + _videoNode.url = _url; + [self doOnPauseSpinnerIsPausedIfPresentWithURL]; +} + +- (void)doOnPauseSpinnerIsPausedIfPresentWithURL +{ + _videoNode.interfaceState = ASInterfaceStateFetchData; [_videoNode play]; - [_videoNode pause]; XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); } + - (void)testOnVideoReadySpinnerIsStoppedAndRemoved { - _videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.asset = _firstAsset; + [self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL]; +} +- (void)testOnVideoReadySpinnerIsStoppedAndRemovedWithURL +{ + _videoNode.url = _url; + [self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL]; +} + +- (void)doOnVideoReadySpinnerIsStoppedAndRemovedWithURL +{ + _videoNode.interfaceState = ASInterfaceStateFetchData; + [_videoNode play]; [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); } + - (void)testPlayerDefaultsToNil { + _videoNode.asset = _firstAsset; + XCTAssertNil(_videoNode.player); +} + +- (void)testPlayerDefaultsToNilWithURL +{ + _videoNode.url = _url; XCTAssertNil(_videoNode.player); } - (void)testPlayerIsCreatedInFetchData { _videoNode.asset = _firstAsset; - _videoNode.interfaceState = ASInterfaceStateFetchData; XCTAssertNotNil(_videoNode.player); } +- (void)testPlayerIsCreatedInFetchDataWithURL +{ + _videoNode.url = _url; + _videoNode.interfaceState = ASInterfaceStateFetchData; + + XCTAssertNotNil(_videoNode.player); +} + + - (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlaying { _videoNode.asset = _firstAsset; + [self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL]; +} +- (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL +{ + _videoNode.url = _url; + [self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL]; +} + +- (void)doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL +{ [_videoNode setInterfaceState:ASInterfaceStateNone]; [_videoNode didLoad]; XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]); } + - (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying { _videoNode.asset = _firstAsset; + [self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying]; +} +- (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlayingWithUrl +{ + _videoNode.url = _url; + [self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying]; +} + +- (void)doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying +{ [_videoNode pause]; [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay]; [_videoNode didLoad]; @@ -157,6 +205,17 @@ - (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay { _videoNode.asset = _firstAsset; + [self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay]; +} + +- (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplayWithURL +{ + _videoNode.url = _url; + [self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay]; +} + +- (void)doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay +{ _videoNode.shouldAutoplay = YES; _videoNode.playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; @@ -169,9 +228,21 @@ XCTAssertTrue(_videoNode.shouldBePlaying); } + - (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater { _videoNode.asset = _firstAsset; + [self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater]; +} + +- (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLaterWithURL +{ + _videoNode.url = _url; + [self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater]; +} + +- (void)doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater +{ [_videoNode play]; [_videoNode interfaceStateDidChange:ASInterfaceStateNone fromState:ASInterfaceStateVisible]; @@ -180,9 +251,21 @@ XCTAssertTrue(_videoNode.shouldBePlaying); } + - (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack { _videoNode.asset = _firstAsset; + [self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack]; +} + +- (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBackWithURL +{ + _videoNode.url = _url; + [self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack]; +} + +- (void)doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack +{ [_videoNode play]; [_videoNode interfaceStateDidChange:ASInterfaceStateVisible fromState:ASInterfaceStateNone]; diff --git a/examples/VideoTableView/Sample/NicCageNode.mm b/examples/VideoTableView/Sample/NicCageNode.mm index c2277762ed..f430a10db9 100644 --- a/examples/VideoTableView/Sample/NicCageNode.mm +++ b/examples/VideoTableView/Sample/NicCageNode.mm @@ -80,16 +80,43 @@ static const CGFloat kInnerPadding = 10.0f; return nil; _kittenSize = size; - - _videoNode = [[ASVideoNode alloc] init]; -// _videoNode.shouldAutoplay = YES; + + u_int32_t videoInitMethod = arc4random_uniform(3); + u_int32_t autoPlay = arc4random_uniform(2); + NSArray* methodArray = @[@"AVAsset", @"File URL", @"HLS URL"]; + NSArray* autoPlayArray = @[@"paused", @"auto play"]; + + switch (videoInitMethod) { + case 0: + // Construct an AVAsset from a URL + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]]; + break; + + case 1: + // Construct the video node directly from the .mp4 URL + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.url = [NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]; + break; + + case 2: + // Construct the video node from an HTTP Live Streaming URL + // URL from https://developer.apple.com/library/ios/documentation/AudioVideo/Conceptual/AVFoundationPG/Articles/02_Playback.html + _videoNode = [[ASVideoNode alloc] init]; + _videoNode.url = [NSURL URLWithString:@"http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"]; + break; + } + + if (autoPlay == 1) + _videoNode.shouldAutoplay = YES; + + _videoNode.shouldAutorepeat = YES; _videoNode.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); - _videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]]; [self addSubnode:_videoNode]; _textNode = [[ASTextNode alloc] init]; - _textNode.attributedString = [[NSAttributedString alloc] initWithString:[self kittyIpsum] + _textNode.attributedString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ %@ %@", methodArray[videoInitMethod], autoPlayArray[autoPlay], [self kittyIpsum]] attributes:[self textStyle]]; [self addSubnode:_textNode]; diff --git a/examples/Videos/Sample/ViewController.m b/examples/Videos/Sample/ViewController.m index 74e4ced00f..def29c0122 100644 --- a/examples/Videos/Sample/ViewController.m +++ b/examples/Videos/Sample/ViewController.m @@ -33,9 +33,9 @@ - (ASVideoNode *)guitarVideo; { + AVAsset* asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"]]; ASVideoNode *videoNode = [[ASVideoNode alloc] init]; - - videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"]]; + videoNode.asset = asset; videoNode.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height/3); @@ -48,12 +48,12 @@ - (ASVideoNode *)nicCageVideo; { + AVAsset* asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-753fe655-86bb-46da-89b7-aa59c60e49c0-niccage.mp4"]]; ASVideoNode *nicCageVideo = [[ASVideoNode alloc] init]; + nicCageVideo.asset = asset; nicCageVideo.delegate = self; - nicCageVideo.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://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.gravity = AVLayerVideoGravityResize; @@ -68,10 +68,9 @@ - (ASVideoNode *)simonVideo; { - ASVideoNode *simonVideo = [[ASVideoNode alloc] init]; - NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"simon" ofType:@"mp4"]]; - simonVideo.asset = [AVAsset assetWithURL:url]; + ASVideoNode *simonVideo = [[ASVideoNode alloc] init]; + simonVideo.url = url; simonVideo.frame = CGRectMake(0, [UIScreen mainScreen].bounds.size.height - ([UIScreen mainScreen].bounds.size.height/3), [UIScreen mainScreen].bounds.size.width/2, [UIScreen mainScreen].bounds.size.height/3);