Merge pull request #1323 from gazreese/AVPlayerItem-construction

[ASVideoNode] Add support for HTTP Live Streaming Video
This commit is contained in:
appleguy 2016-04-17 19:30:23 -07:00
commit 94d0d908dc
5 changed files with 366 additions and 153 deletions

View File

@ -18,7 +18,10 @@
// in an issue on GitHub: https://github.com/facebook/AsyncDisplayKit/issues // in an issue on GitHub: https://github.com/facebook/AsyncDisplayKit/issues
@interface ASVideoNode : ASControlNode @interface ASVideoNode : ASControlNode
@property (atomic, strong, readwrite) NSURL *url;
@property (atomic, strong, readwrite) AVAsset *asset; @property (atomic, strong, readwrite) AVAsset *asset;
@property (atomic, strong, readonly) AVPlayer *player; @property (atomic, strong, readonly) AVPlayer *player;
@property (atomic, strong, readonly) AVPlayerItem *currentItem; @property (atomic, strong, readonly) AVPlayerItem *currentItem;
@ -34,6 +37,8 @@
@property (atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate; @property (atomic, weak, readwrite) id<ASVideoNodeDelegate> delegate;
- (instancetype)init;
- (void)play; - (void)play;
- (void)pause; - (void)pause;

View File

@ -23,8 +23,9 @@
BOOL _muted; BOOL _muted;
AVAsset *_asset; AVAsset *_asset;
NSURL *_url;
AVPlayerItem *_currentItem; AVPlayerItem *_currentPlayerItem;
AVPlayer *_player; AVPlayer *_player;
ASImageNode *_placeholderImageNode; ASImageNode *_placeholderImageNode;
@ -41,70 +42,81 @@
@implementation ASVideoNode @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 - (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.playButton = [[ASDefaultPlayButton alloc] init];
self.gravity = AVLayerVideoGravityResizeAspect; self.gravity = AVLayerVideoGravityResizeAspect;
[self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside]; [self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
return self; 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;
}];
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)removePlayerItemObservers
{
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)didLoad
{
[super didLoad];
if (!(newState & ASInterfaceStateVisible)) {
if (oldState & ASInterfaceStateVisible) {
if (_shouldBePlaying) { if (_shouldBePlaying) {
[self pause]; _playerNode = [self constructPlayerNode];
_shouldBePlaying = YES; [self insertSubnode:_playerNode atIndex:0];
} } else if (_asset) {
[(UIActivityIndicatorView *)_spinner.view stopAnimating]; [self setPlaceholderImagefromAsset:_asset];
[_spinner removeFromSupernode];
}
} else {
if (_shouldBePlaying) {
[self play];
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([change[@"new"] integerValue] == AVPlayerItemStatusReadyToPlay) {
if ([self.subnodes containsObject:_spinner]) {
[_spinner removeFromSupernode];
_spinner = nil;
}
}
if ([change[@"new"] integerValue] == AVPlayerItemStatusFailed) {
}
}
- (void)didPlayToEnd:(NSNotification *)notification
{
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];
}
} }
} }
@ -128,29 +140,22 @@
_spinner.position = CGPointMake(bounds.size.width/2, bounds.size.height/2); _spinner.position = CGPointMake(bounds.size.width/2, bounds.size.height/2);
} }
- (void)didLoad - (void)setPlaceholderImagefromAsset:(AVAsset*)asset
{ {
[super didLoad]; ASDN::MutexLocker l(_videoLock);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didPlayToEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; if (!_placeholderImageNode)
_placeholderImageNode = [[ASImageNode alloc] init];
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;
}];
[self insertSubnode:_playerNode atIndex:0];
} else {
dispatch_async(_previewQueue, ^{ dispatch_async(_previewQueue, ^{
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset]; AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset];
imageGenerator.appliesPreferredTrackTransform = YES; imageGenerator.appliesPreferredTrackTransform = YES;
[imageGenerator generateCGImagesAsynchronouslyForTimes:@[[NSValue valueWithCMTime:CMTimeMake(0, 1)]] completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) { NSArray *times = @[[NSValue valueWithCMTime:CMTimeMake(0, 1)]];
[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]; UIImage *theImage = [UIImage imageWithCGImage:image];
_placeholderImageNode = [[ASImageNode alloc] init]; _placeholderImageNode = [[ASImageNode alloc] init];
@ -171,9 +176,59 @@
_placeholderImageNode.frame = self.bounds; _placeholderImageNode.frame = self.bounds;
[self insertSubnode:_placeholderImageNode atIndex:0]; [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) {
}
}
} }
- (void)tapped - (void)tapped
@ -189,18 +244,12 @@
} }
} }
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock
{
ASDisplayNodeAssertNotSupported();
return nil;
}
- (void)fetchData - (void)fetchData
{ {
[super fetchData]; [super fetchData];
@try { @try {
[_currentItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))];
} }
@catch (NSException * __unused exception) { @catch (NSException * __unused exception) {
NSLog(@"unnecessary removal in fetch data"); NSLog(@"unnecessary removal in fetch data");
@ -208,13 +257,13 @@
{ {
ASDN::MutexLocker l(_videoLock); ASDN::MutexLocker l(_videoLock);
_currentItem = [[AVPlayerItem alloc] initWithAsset:_asset]; [self constructCurrentPlayerItemFromInitData];
[_currentItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL]; [_currentPlayerItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
if (_player) { if (_player) {
[_player replaceCurrentItemWithPlayerItem:_currentItem]; [_player replaceCurrentItemWithPlayerItem:_currentPlayerItem];
} else { } else {
_player = [[AVPlayer alloc] initWithPlayerItem:_currentItem]; _player = [[AVPlayer alloc] initWithPlayerItem:_currentPlayerItem];
_player.muted = _muted; _player.muted = _muted;
} }
} }
@ -245,7 +294,8 @@
if (isVisible) { if (isVisible) {
if (_playerNode.isNodeLoaded) { if (_playerNode.isNodeLoaded) {
if (!_player) { if (!_player) {
_player = [AVPlayer playerWithPlayerItem:[[AVPlayerItem alloc] initWithAsset:_asset]]; [self constructCurrentPlayerItemFromInitData];
_player = [AVPlayer playerWithPlayerItem:_currentPlayerItem];
_player.muted = _muted; _player.muted = _muted;
} }
((AVPlayerLayer *)_playerNode.layer).player = _player; ((AVPlayerLayer *)_playerNode.layer).player = _player;
@ -257,6 +307,7 @@
} }
} }
#pragma mark - Video Properties #pragma mark - Video Properties
- (void)setPlayButton:(ASButtonNode *)playButton - (void)setPlayButton:(ASButtonNode *)playButton
@ -302,6 +353,27 @@
return _asset; 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 - (AVPlayer *)player
{ {
ASDN::MutexLocker l(_videoLock); ASDN::MutexLocker l(_videoLock);
@ -355,16 +427,7 @@
} }
if (!_playerNode) { if (!_playerNode) {
_playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ _playerNode = [self constructPlayerNode];
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;
}];
if ([self.subnodes containsObject:_playButton]) { if ([self.subnodes containsObject:_playButton]) {
[self insertSubnode:_playerNode belowSubnode:_playButton]; [self insertSubnode:_playerNode belowSubnode:_playButton];
@ -380,7 +443,7 @@
_playButton.alpha = 0.0; _playButton.alpha = 0.0;
}]; }];
if (![self ready] && _shouldBePlaying && (self.interfaceState & ASInterfaceStateVisible)) { if (![self ready] && _shouldBePlaying && ASInterfaceStateIncludesVisible(self.interfaceState)) {
[self addSubnode:_spinner]; [self addSubnode:_spinner];
[(UIActivityIndicatorView *)_spinner.view startAnimating]; [(UIActivityIndicatorView *)_spinner.view startAnimating];
} }
@ -388,7 +451,7 @@
- (BOOL)ready - (BOOL)ready
{ {
return _currentItem.status == AVPlayerItemStatusReadyToPlay; return _currentPlayerItem.status == AVPlayerItemStatusReadyToPlay;
} }
- (void)pause - (void)pause
@ -410,6 +473,41 @@
return (_player.rate > 0 && !_player.error); 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 #pragma mark - Property Accessors for Tests
- (ASDisplayNode *)spinner - (ASDisplayNode *)spinner
@ -421,13 +519,13 @@
- (AVPlayerItem *)currentItem - (AVPlayerItem *)currentItem
{ {
ASDN::MutexLocker l(_videoLock); ASDN::MutexLocker l(_videoLock);
return _currentItem; return _currentPlayerItem;
} }
- (void)setCurrentItem:(AVPlayerItem *)currentItem - (void)setCurrentItem:(AVPlayerItem *)currentItem
{ {
ASDN::MutexLocker l(_videoLock); ASDN::MutexLocker l(_videoLock);
_currentItem = currentItem; _currentPlayerItem = currentItem;
} }
- (ASDisplayNode *)playerNode - (ASDisplayNode *)playerNode
@ -446,9 +544,10 @@
- (void)dealloc - (void)dealloc
{ {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil]; [self removePlayerItemObservers];
@try { @try {
[_currentItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))]; [_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))];
} }
@catch (NSException * __unused exception) { @catch (NSException * __unused exception) {
NSLog(@"unnecessary removal in dealloc"); NSLog(@"unnecessary removal in dealloc");

View File

@ -16,6 +16,7 @@
ASVideoNode *_videoNode; ASVideoNode *_videoNode;
AVURLAsset *_firstAsset; AVURLAsset *_firstAsset;
AVAsset *_secondAsset; AVAsset *_secondAsset;
NSURL *_url;
} }
@end @end
@ -52,65 +53,75 @@
_videoNode = [[ASVideoNode alloc] init]; _videoNode = [[ASVideoNode alloc] init];
_firstAsset = [AVURLAsset assetWithURL:[NSURL URLWithString:@"firstURL"]]; _firstAsset = [AVURLAsset assetWithURL:[NSURL URLWithString:@"firstURL"]];
_secondAsset = [AVAsset assetWithURL:[NSURL URLWithString:@"secondURL"]]; _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 - (void)testSpinnerDefaultsToNil
{ {
XCTAssertNil(_videoNode.spinner); XCTAssertNil(_videoNode.spinner);
} }
- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode - (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnode
{ {
_videoNode.interfaceState = ASInterfaceStateFetchData;
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl];
}
- (void)testOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl
{
_videoNode.url = _url;
[self doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl];
}
- (void)doOnPlayIfVideoIsNotReadyInitializeSpinnerAndAddAsSubnodeWithUrl
{
_videoNode.interfaceState = ASInterfaceStateFetchData;
[_videoNode play]; [_videoNode play];
XCTAssertNotNil(_videoNode.spinner); XCTAssertNotNil(_videoNode.spinner);
} }
- (void)testOnPauseSpinnerIsPausedIfPresent - (void)testOnPauseSpinnerIsPausedIfPresent
{ {
_videoNode.interfaceState = ASInterfaceStateFetchData;
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doOnPauseSpinnerIsPausedIfPresentWithURL];
}
- (void)testOnPauseSpinnerIsPausedIfPresentWithURL
{
_videoNode.url = _url;
[self doOnPauseSpinnerIsPausedIfPresentWithURL];
}
- (void)doOnPauseSpinnerIsPausedIfPresentWithURL
{
_videoNode.interfaceState = ASInterfaceStateFetchData;
[_videoNode play]; [_videoNode play];
[_videoNode pause]; [_videoNode pause];
XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating);
} }
- (void)testOnVideoReadySpinnerIsStoppedAndRemoved - (void)testOnVideoReadySpinnerIsStoppedAndRemoved
{ {
_videoNode.interfaceState = ASInterfaceStateFetchData;
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL];
}
- (void)testOnVideoReadySpinnerIsStoppedAndRemovedWithURL
{
_videoNode.url = _url;
[self doOnVideoReadySpinnerIsStoppedAndRemovedWithURL];
}
- (void)doOnVideoReadySpinnerIsStoppedAndRemovedWithURL
{
_videoNode.interfaceState = ASInterfaceStateFetchData;
[_videoNode play]; [_videoNode play];
[_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL]; [_videoNode observeValueForKeyPath:@"status" ofObject:[_videoNode currentItem] change:@{@"new" : @(AVPlayerItemStatusReadyToPlay)} context:NULL];
@ -118,34 +129,71 @@
XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating); XCTAssertFalse(((UIActivityIndicatorView *)_videoNode.spinner.view).isAnimating);
} }
- (void)testPlayerDefaultsToNil - (void)testPlayerDefaultsToNil
{ {
_videoNode.asset = _firstAsset;
XCTAssertNil(_videoNode.player);
}
- (void)testPlayerDefaultsToNilWithURL
{
_videoNode.url = _url;
XCTAssertNil(_videoNode.player); XCTAssertNil(_videoNode.player);
} }
- (void)testPlayerIsCreatedInFetchData - (void)testPlayerIsCreatedInFetchData
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
_videoNode.interfaceState = ASInterfaceStateFetchData; _videoNode.interfaceState = ASInterfaceStateFetchData;
XCTAssertNotNil(_videoNode.player); XCTAssertNotNil(_videoNode.player);
} }
- (void)testPlayerIsCreatedInFetchDataWithURL
{
_videoNode.url = _url;
_videoNode.interfaceState = ASInterfaceStateFetchData;
XCTAssertNotNil(_videoNode.player);
}
- (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlaying - (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlaying
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL];
}
- (void)testPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL
{
_videoNode.url = _url;
[self doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL];
}
- (void)doPlayerLayerNodeIsAddedOnDidLoadIfVisibleAndAutoPlayingWithURL
{
[_videoNode setInterfaceState:ASInterfaceStateNone]; [_videoNode setInterfaceState:ASInterfaceStateNone];
[_videoNode didLoad]; [_videoNode didLoad];
XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]); XCTAssert(![_videoNode.subnodes containsObject:_videoNode.playerNode]);
} }
- (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying - (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying];
}
- (void)testPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlayingWithUrl
{
_videoNode.url = _url;
[self doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying];
}
- (void)doPlayerLayerNodeIsNotAddedIfVisibleButShouldNotBePlaying
{
[_videoNode pause]; [_videoNode pause];
[_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay]; [_videoNode setInterfaceState:ASInterfaceStateVisible | ASInterfaceStateDisplay];
[_videoNode didLoad]; [_videoNode didLoad];
@ -157,6 +205,17 @@
- (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay - (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay];
}
- (void)testVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplayWithURL
{
_videoNode.url = _url;
[self doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay];
}
- (void)doVideoStartsPlayingOnDidDidBecomeVisibleWhenShouldAutoplay
{
_videoNode.shouldAutoplay = YES; _videoNode.shouldAutoplay = YES;
_videoNode.playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{ _videoNode.playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{
AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init]; AVPlayerLayer *playerLayer = [[AVPlayerLayer alloc] init];
@ -169,9 +228,21 @@
XCTAssertTrue(_videoNode.shouldBePlaying); XCTAssertTrue(_videoNode.shouldBePlaying);
} }
- (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater - (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater];
}
- (void)testVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLaterWithURL
{
_videoNode.url = _url;
[self doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater];
}
- (void)doVideoShouldPauseWhenItLeavesVisibleButShouldKnowPlayingShouldRestartLater
{
[_videoNode play]; [_videoNode play];
[_videoNode interfaceStateDidChange:ASInterfaceStateNone fromState:ASInterfaceStateVisible]; [_videoNode interfaceStateDidChange:ASInterfaceStateNone fromState:ASInterfaceStateVisible];
@ -180,9 +251,21 @@
XCTAssertTrue(_videoNode.shouldBePlaying); XCTAssertTrue(_videoNode.shouldBePlaying);
} }
- (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack - (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack
{ {
_videoNode.asset = _firstAsset; _videoNode.asset = _firstAsset;
[self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack];
}
- (void)testVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBackWithURL
{
_videoNode.url = _url;
[self doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack];
}
- (void)doVideoThatIsPlayingWhenItLeavesVisibleRangeStartsAgainWhenItComesBack
{
[_videoNode play]; [_videoNode play];
[_videoNode interfaceStateDidChange:ASInterfaceStateVisible fromState:ASInterfaceStateNone]; [_videoNode interfaceStateDidChange:ASInterfaceStateVisible fromState:ASInterfaceStateNone];

View File

@ -81,15 +81,42 @@ static const CGFloat kInnerPadding = 10.0f;
_kittenSize = size; _kittenSize = size;
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 = [[ASVideoNode alloc] init];
// _videoNode.shouldAutoplay = 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"]]; _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();
[self addSubnode:_videoNode]; [self addSubnode:_videoNode];
_textNode = [[ASTextNode alloc] init]; _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]]; attributes:[self textStyle]];
[self addSubnode:_textNode]; [self addSubnode:_textNode];

View File

@ -33,9 +33,9 @@
- (ASVideoNode *)guitarVideo; - (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]; ASVideoNode *videoNode = [[ASVideoNode alloc] init];
videoNode.asset = asset;
videoNode.asset = [AVAsset assetWithURL:[NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"]];
videoNode.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height/3); videoNode.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height/3);
@ -48,12 +48,12 @@
- (ASVideoNode *)nicCageVideo; - (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]; ASVideoNode *nicCageVideo = [[ASVideoNode alloc] init];
nicCageVideo.asset = asset;
nicCageVideo.delegate = self; 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.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; nicCageVideo.gravity = AVLayerVideoGravityResize;
@ -68,10 +68,9 @@
- (ASVideoNode *)simonVideo; - (ASVideoNode *)simonVideo;
{ {
ASVideoNode *simonVideo = [[ASVideoNode alloc] init];
NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"simon" ofType:@"mp4"]]; 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); 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);