[ASVideoPlayerNode] Fixes and improvements (#3135)

* Improvements for ASVideoPlayer, especially around asset handling and API

* Change legacy URL of videos

* Address comments
This commit is contained in:
Michael Schneider 2017-03-05 11:28:03 -08:00 committed by Huy Nguyen
parent f5951d906e
commit 2df392261e
5 changed files with 144 additions and 119 deletions

View File

@ -39,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign) BOOL controlsDisabled; @property (nonatomic, assign) BOOL controlsDisabled;
@property (nonatomic, assign, readonly) BOOL loadAssetWhenNodeBecomesVisible; @property (nonatomic, assign, readonly) BOOL loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state. This flag does nothing.");
#pragma mark - ASVideoNode property proxy #pragma mark - ASVideoNode property proxy
/** /**
@ -52,6 +52,16 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) ASVideoNodePlayerState playerState; @property (nonatomic, assign, readonly) ASVideoNodePlayerState playerState;
@property (nonatomic, assign, readwrite) BOOL shouldAggressivelyRecoverFromStall; @property (nonatomic, assign, readwrite) BOOL shouldAggressivelyRecoverFromStall;
@property (nullable, nonatomic, strong, readwrite) NSURL *placeholderImageURL; @property (nullable, nonatomic, strong, readwrite) NSURL *placeholderImageURL;
@property (nullable, nonatomic, strong, readwrite) AVAsset *asset;
/**
** @abstract The URL with which the asset was initialized.
** @discussion Setting the URL will override the current asset with a newly created AVURLAsset created from the given URL, and AVAsset *asset will point to that newly created AVURLAsset. Please don't set both assetURL and asset.
** @return Current URL the asset was initialized or nil if no URL was given.
**/
@property (nullable, nonatomic, strong, readwrite) NSURL *assetURL;
/// You should never set any value on the backing video node. Use exclusivively the video player node to set properties
@property (nonatomic, strong, readonly) ASVideoNode *videoNode; @property (nonatomic, strong, readonly) ASVideoNode *videoNode;
//! Defaults to 100 //! Defaults to 100
@ -59,12 +69,16 @@ NS_ASSUME_NONNULL_BEGIN
//! Defaults to AVLayerVideoGravityResizeAspect //! Defaults to AVLayerVideoGravityResizeAspect
@property (nonatomic, copy) NSString *gravity; @property (nonatomic, copy) NSString *gravity;
- (instancetype)initWithUrl:(NSURL*)url; #pragma mark - Lifecycle
- (instancetype)initWithAsset:(AVAsset*)asset; - (instancetype)initWithURL:(NSURL *)URL;
- (instancetype)initWithAsset:(AVAsset *)asset;
- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix; - (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix;
- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible;
- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible; #pragma mark Lifecycle Deprecated
- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible; - (instancetype)initWithUrl:(NSURL *)url ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore.");
- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore.");
- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore.");
- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible ASDISPLAYNODE_DEPRECATED_MSG("Asset is always loaded when this node enters preload state, therefore loadAssetWhenNodeBecomesVisible is deprecated and not used anymore.");
#pragma mark - Public API #pragma mark - Public API
- (void)seekToTime:(CGFloat)percentComplete; - (void)seekToTime:(CGFloat)percentComplete;

View File

@ -22,7 +22,7 @@
static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext; static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
@interface ASVideoPlayerNode() <ASVideoNodeDelegate> @interface ASVideoPlayerNode() <ASVideoNodeDelegate, ASVideoPlayerNodeDelegate>
{ {
__weak id<ASVideoPlayerNodeDelegate> _delegate; __weak id<ASVideoPlayerNodeDelegate> _delegate;
@ -53,11 +53,13 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
unsigned int delegateVideoPlayerNodeDidRecoverFromStall:1; unsigned int delegateVideoPlayerNodeDidRecoverFromStall:1;
} _delegateFlags; } _delegateFlags;
NSURL *_url; // The asset passed in the initializer will be assigned as pending asset. As soon as the first
AVAsset *_asset; // preload state happened all further asset handling is made by using the asset of the backing
AVVideoComposition *_videoComposition; // video node
AVAudioMix *_audioMix; AVAsset *_pendingAsset;
// The backing video node. Ideally this is the source of truth and the video player node should
// not handle anything related to asset management
ASVideoNode *_videoNode; ASVideoNode *_videoNode;
NSArray *_neededDefaultControls; NSArray *_neededDefaultControls;
@ -72,7 +74,6 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
ASStackLayoutSpec *_controlFlexGrowSpacerSpec; ASStackLayoutSpec *_controlFlexGrowSpacerSpec;
ASDisplayNode *_spinnerNode; ASDisplayNode *_spinnerNode;
BOOL _loadAssetWhenNodeBecomesVisible;
BOOL _isSeeking; BOOL _isSeeking;
CMTime _duration; CMTime _duration;
@ -95,122 +96,134 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
@dynamic placeholderImageURL; @dynamic placeholderImageURL;
#pragma mark - Lifecycle
- (instancetype)init - (instancetype)init
{ {
if (!(self = [super init])) { if (!(self = [super init])) {
return nil; return nil;
} }
[self _init]; [self _initControlsAndVideoNode];
return self;
}
- (instancetype)initWithUrl:(NSURL*)url
{
if (!(self = [super init])) {
return nil;
}
_url = url;
_asset = [AVAsset assetWithURL:_url];
_loadAssetWhenNodeBecomesVisible = YES;
[self _init];
return self; return self;
} }
- (instancetype)initWithAsset:(AVAsset *)asset - (instancetype)initWithAsset:(AVAsset *)asset
{ {
if (!(self = [super init])) { if (!(self = [self init])) {
return nil; return nil;
} }
_asset = asset; _pendingAsset = asset;
_loadAssetWhenNodeBecomesVisible = YES;
[self _init];
return self; return self;
} }
-(instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix - (instancetype)initWithURL:(NSURL *)URL
{ {
if (!(self = [super init])) { return [self initWithAsset:[AVAsset assetWithURL:URL]];
return nil;
}
_asset = asset;
_videoComposition = videoComposition;
_audioMix = audioMix;
_loadAssetWhenNodeBecomesVisible = YES;
[self _init];
return self;
} }
- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible - (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix
{ {
if (!(self = [super init])) { if (!(self = [self initWithAsset:asset])) {
return nil; return nil;
} }
_url = url; _videoNode.videoComposition = videoComposition;
_asset = [AVAsset assetWithURL:_url]; _videoNode.audioMix = audioMix;
_loadAssetWhenNodeBecomesVisible = loadAssetWhenNodeBecomesVisible;
[self _init];
return self; return self;
} }
- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible - (void)_initControlsAndVideoNode
{
if (!(self = [super init])) {
return nil;
}
_asset = asset;
_loadAssetWhenNodeBecomesVisible = loadAssetWhenNodeBecomesVisible;
[self _init];
return self;
}
-(instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible
{
if (!(self = [super init])) {
return nil;
}
_asset = asset;
_videoComposition = videoComposition;
_audioMix = audioMix;
_loadAssetWhenNodeBecomesVisible = loadAssetWhenNodeBecomesVisible;
[self _init];
return self;
}
- (void)_init
{ {
_defaultControlsColor = [UIColor whiteColor]; _defaultControlsColor = [UIColor whiteColor];
_cachedControls = [[NSMutableDictionary alloc] init]; _cachedControls = [[NSMutableDictionary alloc] init];
_videoNode = [[ASVideoNode alloc] init]; _videoNode = [[ASVideoNode alloc] init];
_videoNode.delegate = self; _videoNode.delegate = self;
if (_loadAssetWhenNodeBecomesVisible == NO) {
_videoNode.asset = _asset;
_videoNode.videoComposition = _videoComposition;
_videoNode.audioMix = _audioMix;
}
[self addSubnode:_videoNode]; [self addSubnode:_videoNode];
} }
#pragma mark Deprecated
- (instancetype)initWithUrl:(NSURL *)url
{
return [self initWithURL:url];
}
- (instancetype)initWithUrl:(NSURL *)url loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible
{
return [self initWithURL:url];
}
- (instancetype)initWithAsset:(AVAsset *)asset loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible
{
return [self initWithAsset:asset];
}
- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix loadAssetWhenNodeBecomesVisible:(BOOL)loadAssetWhenNodeBecomesVisible
{
return [self initWithAsset:asset videoComposition:videoComposition audioMix:audioMix];
}
#pragma mark - Setter / Getter
- (void)setAssetURL:(NSURL *)assetURL
{
ASDisplayNodeAssertMainThread();
self.asset = [AVAsset assetWithURL:assetURL];
}
- (NSURL *)assetURL
{
NSURL *url = nil;
{
ASDN::MutexLocker l(__instanceLock__);
if ([_pendingAsset isKindOfClass:AVURLAsset.class]) {
url = ((AVURLAsset *)_pendingAsset).URL;
}
}
return url ?: _videoNode.assetURL;
}
- (void)setAsset:(AVAsset *)asset
{
ASDisplayNodeAssertMainThread();
__instanceLock__.lock();
// Clean out pending asset
_pendingAsset = nil;
// Set asset based on interface state
if ((ASInterfaceStateIncludesPreload(self.interfaceState))) {
// Don't hold the lock while accessing the subnode
__instanceLock__.unlock();
_videoNode.asset = asset;
return;
}
_pendingAsset = asset;
__instanceLock__.unlock();
}
- (AVAsset *)asset
{
AVAsset *asset = nil;
{
ASDN::MutexLocker l(__instanceLock__);
asset = _pendingAsset;
}
return asset ?: _videoNode.asset;
}
#pragma mark - ASDisplayNode
- (void)didLoad - (void)didLoad
{ {
[super didLoad]; [super didLoad];
@ -220,38 +233,25 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
} }
} }
- (void)didEnterVisibleState - (void)didEnterPreloadState
{ {
[super didEnterVisibleState]; [super didEnterPreloadState];
ASDN::MutexLocker l(__instanceLock__); AVAsset *pendingAsset = nil;
{
if (_loadAssetWhenNodeBecomesVisible) { ASDN::MutexLocker l(__instanceLock__);
if (_asset != _videoNode.asset) { pendingAsset = _pendingAsset;
_videoNode.asset = _asset; _pendingAsset = nil;
}
if (_videoComposition != _videoNode.videoComposition) {
_videoNode.videoComposition = _videoComposition;
}
if (_audioMix != _videoNode.audioMix) {
_videoNode.audioMix = _audioMix;
}
}
}
- (NSArray *)createDefaultControlElementArray
{
if (_delegateFlags.delegateNeededDefaultControls) {
return [_delegate videoPlayerNodeNeededDefaultControls:self];
} }
return @[ @(ASVideoPlayerNodeControlTypePlaybackButton), // If we enter preload state we apply the pending asset to load to the video node so it can start and fetch the asset
@(ASVideoPlayerNodeControlTypeElapsedText), if (pendingAsset != nil && _videoNode.asset != pendingAsset) {
@(ASVideoPlayerNodeControlTypeScrubber), _videoNode.asset = pendingAsset;
@(ASVideoPlayerNodeControlTypeDurationText) ]; }
} }
#pragma mark - UI #pragma mark - UI
- (void)createControls - (void)createControls
{ {
ASDN::MutexLocker l(__instanceLock__); ASDN::MutexLocker l(__instanceLock__);
@ -313,6 +313,18 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
}); });
} }
- (NSArray *)createDefaultControlElementArray
{
if (_delegateFlags.delegateNeededDefaultControls) {
return [_delegate videoPlayerNodeNeededDefaultControls:self];
}
return @[ @(ASVideoPlayerNodeControlTypePlaybackButton),
@(ASVideoPlayerNodeControlTypeElapsedText),
@(ASVideoPlayerNodeControlTypeScrubber),
@(ASVideoPlayerNodeControlTypeDurationText) ];
}
- (void)removeControls - (void)removeControls
{ {
for (ASDisplayNode *node in [_cachedControls objectEnumerator]) { for (ASDisplayNode *node in [_cachedControls objectEnumerator]) {

View File

@ -24,7 +24,7 @@
{ {
self = [super init]; self = [super init];
if (self) { if (self) {
NSString *videoUrlString = @"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"; NSString *videoUrlString = @"https://www.w3schools.com/html/mov_bbb.mp4";
NSString *avatarUrlString = [NSString stringWithFormat:@"https://api.adorable.io/avatars/50/%@",[[NSProcessInfo processInfo] globallyUniqueString]]; NSString *avatarUrlString = [NSString stringWithFormat:@"https://api.adorable.io/avatars/50/%@",[[NSProcessInfo processInfo] globallyUniqueString]];
_title = @"Demo title"; _title = @"Demo title";

View File

@ -69,7 +69,7 @@
_muteButtonNode.style.height = ASDimensionMakeWithPoints(22.0); _muteButtonNode.style.height = ASDimensionMakeWithPoints(22.0);
[_muteButtonNode addTarget:self action:@selector(didTapMuteButton) forControlEvents:ASControlNodeEventTouchUpInside]; [_muteButtonNode addTarget:self action:@selector(didTapMuteButton) forControlEvents:ASControlNodeEventTouchUpInside];
_videoPlayerNode = [[ASVideoPlayerNode alloc] initWithUrl:_videoModel.url loadAssetWhenNodeBecomesVisible:YES]; _videoPlayerNode = [[ASVideoPlayerNode alloc] initWithURL:_videoModel.url];
_videoPlayerNode.delegate = self; _videoPlayerNode.delegate = self;
_videoPlayerNode.backgroundColor = [UIColor blackColor]; _videoPlayerNode.backgroundColor = [UIColor blackColor];
[self addSubnode:_videoPlayerNode]; [self addSubnode:_videoPlayerNode];
@ -142,7 +142,6 @@
- (void)didTapVideoPlayerNode:(ASVideoPlayerNode *)videoPlayer - (void)didTapVideoPlayerNode:(ASVideoPlayerNode *)videoPlayer
{ {
if (_videoPlayerNode.playerState == ASVideoNodePlayerStatePlaying) { if (_videoPlayerNode.playerState == ASVideoNodePlayerStatePlaying) {
NSLog(@"TRANSITION");
_videoPlayerNode.controlsDisabled = !_videoPlayerNode.controlsDisabled; _videoPlayerNode.controlsDisabled = !_videoPlayerNode.controlsDisabled;
[_videoPlayerNode pause]; [_videoPlayerNode pause];
} else { } else {

View File

@ -87,9 +87,9 @@
return _videoPlayerNode; return _videoPlayerNode;
} }
NSURL *fileUrl = [NSURL URLWithString:@"https://files.parsetfss.com/8a8a3b0c-619e-4e4d-b1d5-1b5ba9bf2b42/tfss-3045b261-7e93-4492-b7e5-5d6358376c9f-editedLiveAndDie.mov"]; NSURL *fileUrl = [NSURL URLWithString:@"https://www.w3schools.com/html/mov_bbb.mp4"];
_videoPlayerNode = [[ASVideoPlayerNode alloc] initWithUrl:fileUrl]; _videoPlayerNode = [[ASVideoPlayerNode alloc] initWithURL:fileUrl];
_videoPlayerNode.delegate = self; _videoPlayerNode.delegate = self;
// _videoPlayerNode.disableControls = YES; // _videoPlayerNode.disableControls = YES;
// //