mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

git-subtree-dir: submodules/AsyncDisplayKit git-subtree-mainline: d06f423e0ed3df1fed9bd10d79ee312a9179b632 git-subtree-split: 02bedc12816e251ad71777f9d2578329b6d2bef6
1025 lines
31 KiB
Plaintext
1025 lines
31 KiB
Plaintext
//
|
|
// ASVideoPlayerNode.mm
|
|
// Texture
|
|
//
|
|
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
|
|
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
|
|
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#ifndef MINIMAL_ASDK
|
|
|
|
#import <AsyncDisplayKit/ASVideoPlayerNode.h>
|
|
|
|
#if AS_USE_VIDEO
|
|
#if TARGET_OS_IOS
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
#import <AsyncDisplayKit/AsyncDisplayKit.h>
|
|
#import <AsyncDisplayKit/ASDefaultPlaybackButton.h>
|
|
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
|
|
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
|
|
#import <AsyncDisplayKit/ASThread.h>
|
|
|
|
static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
|
|
|
|
@interface ASVideoPlayerNode() <ASVideoNodeDelegate, ASVideoPlayerNodeDelegate>
|
|
{
|
|
__weak id<ASVideoPlayerNodeDelegate> _delegate;
|
|
|
|
struct {
|
|
unsigned int delegateNeededDefaultControls:1;
|
|
unsigned int delegateCustomControls:1;
|
|
unsigned int delegateSpinnerTintColor:1;
|
|
unsigned int delegateSpinnerStyle:1;
|
|
unsigned int delegatePlaybackButtonTint:1;
|
|
unsigned int delegateFullScreenButtonImage:1;
|
|
unsigned int delegateScrubberMaximumTrackTintColor:1;
|
|
unsigned int delegateScrubberMinimumTrackTintColor:1;
|
|
unsigned int delegateScrubberThumbTintColor:1;
|
|
unsigned int delegateScrubberThumbImage:1;
|
|
unsigned int delegateTimeLabelAttributes:1;
|
|
unsigned int delegateTimeLabelAttributedString:1;
|
|
unsigned int delegateLayoutSpecForControls:1;
|
|
unsigned int delegateVideoNodeDidPlayToTime:1;
|
|
unsigned int delegateVideoNodeWillChangeState:1;
|
|
unsigned int delegateVideoNodeShouldChangeState:1;
|
|
unsigned int delegateVideoNodePlaybackDidFinish:1;
|
|
unsigned int delegateDidTapVideoPlayerNode:1;
|
|
unsigned int delegateDidTapFullScreenButtonNode:1;
|
|
unsigned int delegateVideoPlayerNodeDidSetCurrentItem:1;
|
|
unsigned int delegateVideoPlayerNodeDidStallAtTimeInterval:1;
|
|
unsigned int delegateVideoPlayerNodeDidStartInitialLoading:1;
|
|
unsigned int delegateVideoPlayerNodeDidFinishInitialLoading:1;
|
|
unsigned int delegateVideoPlayerNodeDidRecoverFromStall:1;
|
|
} _delegateFlags;
|
|
|
|
// The asset passed in the initializer will be assigned as pending asset. As soon as the first
|
|
// preload state happened all further asset handling is made by using the asset of the backing
|
|
// video node
|
|
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;
|
|
|
|
NSArray *_neededDefaultControls;
|
|
|
|
NSMutableDictionary *_cachedControls;
|
|
|
|
ASDefaultPlaybackButton *_playbackButtonNode;
|
|
ASButtonNode *_fullScreenButtonNode;
|
|
ASTextNode *_elapsedTextNode;
|
|
ASTextNode *_durationTextNode;
|
|
ASDisplayNode *_scrubberNode;
|
|
ASStackLayoutSpec *_controlFlexGrowSpacerSpec;
|
|
ASDisplayNode *_spinnerNode;
|
|
|
|
BOOL _isSeeking;
|
|
CMTime _duration;
|
|
|
|
BOOL _controlsDisabled;
|
|
|
|
BOOL _shouldAutoPlay;
|
|
BOOL _shouldAutoRepeat;
|
|
BOOL _muted;
|
|
int32_t _periodicTimeObserverTimescale;
|
|
NSString *_gravity;
|
|
|
|
BOOL _shouldAggressivelyRecoverFromStall;
|
|
|
|
UIColor *_defaultControlsColor;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation ASVideoPlayerNode
|
|
|
|
@dynamic placeholderImageURL;
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (!(self = [super init])) {
|
|
return nil;
|
|
}
|
|
|
|
[self _initControlsAndVideoNode];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithAsset:(AVAsset *)asset
|
|
{
|
|
if (!(self = [self init])) {
|
|
return nil;
|
|
}
|
|
|
|
_pendingAsset = asset;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithURL:(NSURL *)URL
|
|
{
|
|
return [self initWithAsset:[AVAsset assetWithURL:URL]];
|
|
}
|
|
|
|
- (instancetype)initWithAsset:(AVAsset *)asset videoComposition:(AVVideoComposition *)videoComposition audioMix:(AVAudioMix *)audioMix
|
|
{
|
|
if (!(self = [self initWithAsset:asset])) {
|
|
return nil;
|
|
}
|
|
|
|
_videoNode.videoComposition = videoComposition;
|
|
_videoNode.audioMix = audioMix;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_initControlsAndVideoNode
|
|
{
|
|
_defaultControlsColor = [UIColor whiteColor];
|
|
_cachedControls = [[NSMutableDictionary alloc] init];
|
|
|
|
_videoNode = [[ASVideoNode alloc] init];
|
|
_videoNode.delegate = self;
|
|
[self addSubnode:_videoNode];
|
|
}
|
|
|
|
#pragma mark - Setter / Getter
|
|
|
|
- (void)setAssetURL:(NSURL *)assetURL
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
self.asset = [AVAsset assetWithURL:assetURL];
|
|
}
|
|
|
|
- (NSURL *)assetURL
|
|
{
|
|
NSURL *url = nil;
|
|
{
|
|
ASLockScopeSelf();
|
|
if ([_pendingAsset isKindOfClass:AVURLAsset.class]) {
|
|
url = ((AVURLAsset *)_pendingAsset).URL;
|
|
}
|
|
}
|
|
|
|
return url ?: _videoNode.assetURL;
|
|
}
|
|
|
|
- (void)setAsset:(AVAsset *)asset
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
|
|
[self 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
|
|
[self unlock];
|
|
_videoNode.asset = asset;
|
|
return;
|
|
}
|
|
|
|
_pendingAsset = asset;
|
|
[self unlock];
|
|
}
|
|
|
|
- (AVAsset *)asset
|
|
{
|
|
return ASLockedSelf(_pendingAsset) ?: _videoNode.asset;
|
|
}
|
|
|
|
#pragma mark - ASDisplayNode
|
|
|
|
- (void)didLoad
|
|
{
|
|
[super didLoad];
|
|
|
|
[self createControls];
|
|
}
|
|
|
|
- (void)didEnterPreloadState
|
|
{
|
|
[super didEnterPreloadState];
|
|
|
|
AVAsset *pendingAsset = nil;
|
|
{
|
|
ASLockScopeSelf();
|
|
pendingAsset = _pendingAsset;
|
|
_pendingAsset = nil;
|
|
}
|
|
|
|
// If we enter preload state we apply the pending asset to load to the video node so it can start and fetch the asset
|
|
if (pendingAsset != nil && _videoNode.asset != pendingAsset) {
|
|
_videoNode.asset = pendingAsset;
|
|
}
|
|
}
|
|
|
|
- (BOOL)supportsLayerBacking
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark - UI
|
|
|
|
- (void)createControls
|
|
{
|
|
{
|
|
ASLockScopeSelf();
|
|
|
|
if (_controlsDisabled) {
|
|
return;
|
|
}
|
|
|
|
if (_neededDefaultControls == nil) {
|
|
_neededDefaultControls = [self createDefaultControlElementArray];
|
|
}
|
|
|
|
if (_cachedControls == nil) {
|
|
_cachedControls = [[NSMutableDictionary alloc] init];
|
|
}
|
|
|
|
for (id object in _neededDefaultControls) {
|
|
ASVideoPlayerNodeControlType type = (ASVideoPlayerNodeControlType)[object integerValue];
|
|
switch (type) {
|
|
case ASVideoPlayerNodeControlTypePlaybackButton:
|
|
[self _locked_createPlaybackButton];
|
|
break;
|
|
case ASVideoPlayerNodeControlTypeElapsedText:
|
|
[self _locked_createElapsedTextField];
|
|
break;
|
|
case ASVideoPlayerNodeControlTypeDurationText:
|
|
[self _locked_createDurationTextField];
|
|
break;
|
|
case ASVideoPlayerNodeControlTypeScrubber:
|
|
[self _locked_createScrubber];
|
|
break;
|
|
case ASVideoPlayerNodeControlTypeFullScreenButton:
|
|
[self _locked_createFullScreenButton];
|
|
break;
|
|
case ASVideoPlayerNodeControlTypeFlexGrowSpacer:
|
|
[self _locked_createControlFlexGrowSpacer];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (_delegateFlags.delegateCustomControls && _delegateFlags.delegateLayoutSpecForControls) {
|
|
NSDictionary *customControls = [_delegate videoPlayerNodeCustomControls:self];
|
|
std::vector<ASDisplayNode *> subnodes;
|
|
for (id key in customControls) {
|
|
id node = customControls[key];
|
|
if (![node isKindOfClass:[ASDisplayNode class]]) {
|
|
continue;
|
|
}
|
|
|
|
subnodes.push_back(node);
|
|
[_cachedControls setObject:node forKey:key];
|
|
}
|
|
|
|
{
|
|
ASUnlockScope(self);
|
|
for (ASDisplayNode *subnode : subnodes) {
|
|
[self addSubnode:subnode];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ASPerformBlockOnMainThread(^{
|
|
[self setNeedsLayout];
|
|
});
|
|
}
|
|
|
|
- (NSArray *)createDefaultControlElementArray
|
|
{
|
|
if (_delegateFlags.delegateNeededDefaultControls) {
|
|
return [_delegate videoPlayerNodeNeededDefaultControls:self];
|
|
}
|
|
|
|
return @[ @(ASVideoPlayerNodeControlTypePlaybackButton),
|
|
@(ASVideoPlayerNodeControlTypeElapsedText),
|
|
@(ASVideoPlayerNodeControlTypeScrubber),
|
|
@(ASVideoPlayerNodeControlTypeDurationText) ];
|
|
}
|
|
|
|
- (void)removeControls
|
|
{
|
|
NSMutableDictionary *cachedControls = nil;
|
|
{
|
|
ASLockScope(self);
|
|
|
|
// Grab the cached controls for removing it
|
|
cachedControls = [_cachedControls copy];
|
|
[self _locked_cleanCachedControls];
|
|
}
|
|
|
|
for (ASDisplayNode *node in [cachedControls objectEnumerator]) {
|
|
[node removeFromSupernode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_cleanCachedControls
|
|
{
|
|
[_cachedControls removeAllObjects];
|
|
|
|
_playbackButtonNode = nil;
|
|
_fullScreenButtonNode = nil;
|
|
_elapsedTextNode = nil;
|
|
_durationTextNode = nil;
|
|
_scrubberNode = nil;
|
|
}
|
|
|
|
- (void)_locked_createPlaybackButton
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_playbackButtonNode == nil) {
|
|
_playbackButtonNode = [[ASDefaultPlaybackButton alloc] init];
|
|
_playbackButtonNode.style.preferredSize = CGSizeMake(16.0, 22.0);
|
|
|
|
if (_delegateFlags.delegatePlaybackButtonTint) {
|
|
_playbackButtonNode.tintColor = [_delegate videoPlayerNodePlaybackButtonTint:self];
|
|
} else {
|
|
_playbackButtonNode.tintColor = _defaultControlsColor;
|
|
}
|
|
|
|
if (_videoNode.playerState == ASVideoNodePlayerStatePlaying) {
|
|
_playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePause;
|
|
}
|
|
|
|
[_playbackButtonNode addTarget:self action:@selector(didTapPlaybackButton:) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
[_cachedControls setObject:_playbackButtonNode forKey:@(ASVideoPlayerNodeControlTypePlaybackButton)];
|
|
}
|
|
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:_playbackButtonNode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_createFullScreenButton
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_fullScreenButtonNode == nil) {
|
|
_fullScreenButtonNode = [[ASButtonNode alloc] init];
|
|
_fullScreenButtonNode.style.preferredSize = CGSizeMake(16.0, 22.0);
|
|
|
|
if (_delegateFlags.delegateFullScreenButtonImage) {
|
|
[_fullScreenButtonNode setImage:[_delegate videoPlayerNodeFullScreenButtonImage:self] forState:UIControlStateNormal];
|
|
}
|
|
|
|
[_fullScreenButtonNode addTarget:self action:@selector(didTapFullScreenButton:) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
[_cachedControls setObject:_fullScreenButtonNode forKey:@(ASVideoPlayerNodeControlTypeFullScreenButton)];
|
|
}
|
|
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:_fullScreenButtonNode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_createElapsedTextField
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_elapsedTextNode == nil) {
|
|
_elapsedTextNode = [[ASTextNode alloc] init];
|
|
_elapsedTextNode.attributedText = [self timeLabelAttributedStringForString:@"00:00"
|
|
forControlType:ASVideoPlayerNodeControlTypeElapsedText];
|
|
_elapsedTextNode.truncationMode = NSLineBreakByClipping;
|
|
|
|
[_cachedControls setObject:_elapsedTextNode forKey:@(ASVideoPlayerNodeControlTypeElapsedText)];
|
|
}
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:_elapsedTextNode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_createDurationTextField
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_durationTextNode == nil) {
|
|
_durationTextNode = [[ASTextNode alloc] init];
|
|
_durationTextNode.attributedText = [self timeLabelAttributedStringForString:@"00:00"
|
|
forControlType:ASVideoPlayerNodeControlTypeDurationText];
|
|
_durationTextNode.truncationMode = NSLineBreakByClipping;
|
|
|
|
[_cachedControls setObject:_durationTextNode forKey:@(ASVideoPlayerNodeControlTypeDurationText)];
|
|
}
|
|
[self updateDurationTimeLabel];
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:_durationTextNode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_createScrubber
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_scrubberNode == nil) {
|
|
__weak __typeof__(self) weakSelf = self;
|
|
_scrubberNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull {
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
|
|
UISlider *slider = [[UISlider alloc] initWithFrame:CGRectZero];
|
|
slider.minimumValue = 0.0;
|
|
slider.maximumValue = 1.0;
|
|
|
|
if (_delegateFlags.delegateScrubberMinimumTrackTintColor) {
|
|
slider.minimumTrackTintColor = [strongSelf.delegate videoPlayerNodeScrubberMinimumTrackTint:strongSelf];
|
|
}
|
|
|
|
if (_delegateFlags.delegateScrubberMaximumTrackTintColor) {
|
|
slider.maximumTrackTintColor = [strongSelf.delegate videoPlayerNodeScrubberMaximumTrackTint:strongSelf];
|
|
}
|
|
|
|
if (_delegateFlags.delegateScrubberThumbTintColor) {
|
|
slider.thumbTintColor = [strongSelf.delegate videoPlayerNodeScrubberThumbTint:strongSelf];
|
|
}
|
|
|
|
if (_delegateFlags.delegateScrubberThumbImage) {
|
|
UIImage *thumbImage = [strongSelf.delegate videoPlayerNodeScrubberThumbImage:strongSelf];
|
|
[slider setThumbImage:thumbImage forState:UIControlStateNormal];
|
|
}
|
|
|
|
|
|
[slider addTarget:strongSelf action:@selector(beginSeek) forControlEvents:UIControlEventTouchDown];
|
|
[slider addTarget:strongSelf action:@selector(endSeek) forControlEvents:UIControlEventTouchUpInside|UIControlEventTouchUpOutside|UIControlEventTouchCancel];
|
|
[slider addTarget:strongSelf action:@selector(seekTimeDidChange:) forControlEvents:UIControlEventValueChanged];
|
|
|
|
return slider;
|
|
}];
|
|
|
|
_scrubberNode.style.flexShrink = 1;
|
|
|
|
[_cachedControls setObject:_scrubberNode forKey:@(ASVideoPlayerNodeControlTypeScrubber)];
|
|
}
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:_scrubberNode];
|
|
}
|
|
}
|
|
|
|
- (void)_locked_createControlFlexGrowSpacer
|
|
{
|
|
ASAssertLocked(__instanceLock__);
|
|
|
|
if (_controlFlexGrowSpacerSpec == nil) {
|
|
_controlFlexGrowSpacerSpec = [[ASStackLayoutSpec alloc] init];
|
|
_controlFlexGrowSpacerSpec.style.flexGrow = 1.0;
|
|
}
|
|
|
|
[_cachedControls setObject:_controlFlexGrowSpacerSpec forKey:@(ASVideoPlayerNodeControlTypeFlexGrowSpacer)];
|
|
}
|
|
|
|
- (void)updateDurationTimeLabel
|
|
{
|
|
if (!_durationTextNode) {
|
|
return;
|
|
}
|
|
NSString *formattedDuration = [self timeStringForCMTime:_duration forTimeLabelType:ASVideoPlayerNodeControlTypeDurationText];
|
|
_durationTextNode.attributedText = [self timeLabelAttributedStringForString:formattedDuration forControlType:ASVideoPlayerNodeControlTypeDurationText];
|
|
}
|
|
|
|
- (void)updateElapsedTimeLabel:(NSTimeInterval)seconds
|
|
{
|
|
if (!_elapsedTextNode) {
|
|
return;
|
|
}
|
|
NSString *formattedElapsed = [self timeStringForCMTime:CMTimeMakeWithSeconds( seconds, _videoNode.periodicTimeObserverTimescale ) forTimeLabelType:ASVideoPlayerNodeControlTypeElapsedText];
|
|
_elapsedTextNode.attributedText = [self timeLabelAttributedStringForString:formattedElapsed forControlType:ASVideoPlayerNodeControlTypeElapsedText];
|
|
}
|
|
|
|
- (NSAttributedString*)timeLabelAttributedStringForString:(NSString*)string forControlType:(ASVideoPlayerNodeControlType)controlType
|
|
{
|
|
NSDictionary *options;
|
|
if (_delegateFlags.delegateTimeLabelAttributes) {
|
|
options = [_delegate videoPlayerNodeTimeLabelAttributes:self timeLabelType:controlType];
|
|
} else {
|
|
options = @{
|
|
NSFontAttributeName : [UIFont systemFontOfSize:12.0],
|
|
NSForegroundColorAttributeName: _defaultControlsColor
|
|
};
|
|
}
|
|
|
|
|
|
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:options];
|
|
|
|
return attributedString;
|
|
}
|
|
|
|
#pragma mark - ASVideoNodeDelegate
|
|
- (void)videoNode:(ASVideoNode *)videoNode willChangePlayerState:(ASVideoNodePlayerState)state toState:(ASVideoNodePlayerState)toState
|
|
{
|
|
if (_delegateFlags.delegateVideoNodeWillChangeState) {
|
|
[_delegate videoPlayerNode:self willChangeVideoNodeState:state toVideoNodeState:toState];
|
|
}
|
|
|
|
if (toState == ASVideoNodePlayerStateReadyToPlay) {
|
|
_duration = _videoNode.currentItem.duration;
|
|
[self updateDurationTimeLabel];
|
|
}
|
|
|
|
if (toState == ASVideoNodePlayerStatePlaying) {
|
|
_playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePause;
|
|
[self removeSpinner];
|
|
} else if (toState != ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying && toState != ASVideoNodePlayerStateReadyToPlay) {
|
|
_playbackButtonNode.buttonType = ASDefaultPlaybackButtonTypePlay;
|
|
}
|
|
|
|
if (toState == ASVideoNodePlayerStateLoading || toState == ASVideoNodePlayerStateInitialLoading) {
|
|
[self showSpinner];
|
|
}
|
|
|
|
if (toState == ASVideoNodePlayerStateReadyToPlay || toState == ASVideoNodePlayerStatePaused || toState == ASVideoNodePlayerStatePlaybackLikelyToKeepUpButNotPlaying) {
|
|
[self removeSpinner];
|
|
}
|
|
}
|
|
|
|
- (BOOL)videoNode:(ASVideoNode *)videoNode shouldChangePlayerStateTo:(ASVideoNodePlayerState)state
|
|
{
|
|
if (_delegateFlags.delegateVideoNodeShouldChangeState) {
|
|
return [_delegate videoPlayerNode:self shouldChangeVideoNodeStateTo:state];
|
|
}
|
|
return YES;
|
|
}
|
|
|
|
- (void)videoNode:(ASVideoNode *)videoNode didPlayToTimeInterval:(NSTimeInterval)timeInterval
|
|
{
|
|
if (_delegateFlags.delegateVideoNodeDidPlayToTime) {
|
|
[_delegate videoPlayerNode:self didPlayToTime:_videoNode.player.currentTime];
|
|
}
|
|
|
|
if (_isSeeking) {
|
|
return;
|
|
}
|
|
|
|
if (_elapsedTextNode) {
|
|
[self updateElapsedTimeLabel:timeInterval];
|
|
}
|
|
|
|
if (_scrubberNode) {
|
|
[(UISlider*)_scrubberNode.view setValue:( timeInterval / CMTimeGetSeconds(_duration) ) animated:NO];
|
|
}
|
|
}
|
|
|
|
- (void)videoDidPlayToEnd:(ASVideoNode *)videoNode
|
|
{
|
|
if (_delegateFlags.delegateVideoNodePlaybackDidFinish) {
|
|
[_delegate videoPlayerNodeDidPlayToEnd:self];
|
|
}
|
|
}
|
|
|
|
- (void)didTapVideoNode:(ASVideoNode *)videoNode
|
|
{
|
|
if (_delegateFlags.delegateDidTapVideoPlayerNode) {
|
|
[_delegate didTapVideoPlayerNode:self];
|
|
} else {
|
|
[self togglePlayPause];
|
|
}
|
|
}
|
|
|
|
- (void)videoNode:(ASVideoNode *)videoNode didSetCurrentItem:(AVPlayerItem *)currentItem
|
|
{
|
|
if (_delegateFlags.delegateVideoPlayerNodeDidSetCurrentItem) {
|
|
[_delegate videoPlayerNode:self didSetCurrentItem:currentItem];
|
|
}
|
|
}
|
|
|
|
- (void)videoNode:(ASVideoNode *)videoNode didStallAtTimeInterval:(NSTimeInterval)timeInterval
|
|
{
|
|
if (_delegateFlags.delegateVideoPlayerNodeDidStallAtTimeInterval) {
|
|
[_delegate videoPlayerNode:self didStallAtTimeInterval:timeInterval];
|
|
}
|
|
}
|
|
|
|
- (void)videoNodeDidStartInitialLoading:(ASVideoNode *)videoNode
|
|
{
|
|
if (_delegateFlags.delegateVideoPlayerNodeDidStartInitialLoading) {
|
|
[_delegate videoPlayerNodeDidStartInitialLoading:self];
|
|
}
|
|
}
|
|
|
|
- (void)videoNodeDidFinishInitialLoading:(ASVideoNode *)videoNode
|
|
{
|
|
if (_delegateFlags.delegateVideoPlayerNodeDidFinishInitialLoading) {
|
|
[_delegate videoPlayerNodeDidFinishInitialLoading:self];
|
|
}
|
|
}
|
|
|
|
- (void)videoNodeDidRecoverFromStall:(ASVideoNode *)videoNode
|
|
{
|
|
if (_delegateFlags.delegateVideoPlayerNodeDidRecoverFromStall) {
|
|
[_delegate videoPlayerNodeDidRecoverFromStall:self];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
- (void)togglePlayPause
|
|
{
|
|
if (_videoNode.playerState == ASVideoNodePlayerStatePlaying) {
|
|
[_videoNode pause];
|
|
} else {
|
|
[_videoNode play];
|
|
}
|
|
}
|
|
|
|
- (void)showSpinner
|
|
{
|
|
ASLockScopeSelf();
|
|
|
|
if (!_spinnerNode) {
|
|
__weak __typeof__(self) weakSelf = self;
|
|
_spinnerNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
|
|
__typeof__(self) strongSelf = weakSelf;
|
|
UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init];
|
|
spinnnerView.backgroundColor = [UIColor clearColor];
|
|
|
|
if (_delegateFlags.delegateSpinnerTintColor) {
|
|
spinnnerView.color = [_delegate videoPlayerNodeSpinnerTint:strongSelf];
|
|
} else {
|
|
spinnnerView.color = _defaultControlsColor;
|
|
}
|
|
|
|
if (_delegateFlags.delegateSpinnerStyle) {
|
|
spinnnerView.activityIndicatorViewStyle = [_delegate videoPlayerNodeSpinnerStyle:strongSelf];
|
|
}
|
|
|
|
return spinnnerView;
|
|
}];
|
|
_spinnerNode.style.preferredSize = CGSizeMake(44.0, 44.0);
|
|
|
|
const auto spinnerNode = _spinnerNode;
|
|
{
|
|
ASUnlockScope(self);
|
|
[self addSubnode:spinnerNode];
|
|
[self setNeedsLayout];
|
|
}
|
|
}
|
|
[(UIActivityIndicatorView *)_spinnerNode.view startAnimating];
|
|
}
|
|
|
|
- (void)removeSpinner
|
|
{
|
|
ASDisplayNode *spinnerNode = nil;
|
|
{
|
|
ASLockScopeSelf();
|
|
if (!_spinnerNode) {
|
|
return;
|
|
}
|
|
|
|
spinnerNode = _spinnerNode;
|
|
_spinnerNode = nil;
|
|
}
|
|
|
|
[spinnerNode removeFromSupernode];
|
|
}
|
|
|
|
- (void)didTapPlaybackButton:(ASControlNode*)node
|
|
{
|
|
[self togglePlayPause];
|
|
}
|
|
|
|
- (void)didTapFullScreenButton:(ASButtonNode*)node
|
|
{
|
|
if (_delegateFlags.delegateDidTapFullScreenButtonNode) {
|
|
[_delegate didTapFullScreenButtonNode:node];
|
|
}
|
|
}
|
|
|
|
- (void)beginSeek
|
|
{
|
|
_isSeeking = YES;
|
|
}
|
|
|
|
- (void)endSeek
|
|
{
|
|
_isSeeking = NO;
|
|
}
|
|
|
|
- (void)seekTimeDidChange:(UISlider*)slider
|
|
{
|
|
CGFloat percentage = slider.value * 100;
|
|
[self seekToTime:percentage];
|
|
}
|
|
|
|
#pragma mark - Public API
|
|
- (void)seekToTime:(CGFloat)percentComplete
|
|
{
|
|
CGFloat seconds = ( CMTimeGetSeconds(_duration) * percentComplete ) / 100;
|
|
|
|
[self updateElapsedTimeLabel:seconds];
|
|
[_videoNode.player seekToTime:CMTimeMakeWithSeconds(seconds, _videoNode.periodicTimeObserverTimescale)];
|
|
|
|
if (_videoNode.playerState != ASVideoNodePlayerStatePlaying) {
|
|
[self togglePlayPause];
|
|
}
|
|
}
|
|
|
|
- (void)play
|
|
{
|
|
[_videoNode play];
|
|
}
|
|
|
|
- (void)pause
|
|
{
|
|
[_videoNode pause];
|
|
}
|
|
|
|
- (BOOL)isPlaying
|
|
{
|
|
return [_videoNode isPlaying];
|
|
}
|
|
|
|
- (void)resetToPlaceholder
|
|
{
|
|
[_videoNode resetToPlaceholder];
|
|
}
|
|
|
|
- (NSArray *)controlsForLayoutSpec
|
|
{
|
|
NSMutableArray *controls = [[NSMutableArray alloc] initWithCapacity:_cachedControls.count];
|
|
|
|
if (_cachedControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]) {
|
|
[controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypePlaybackButton) ]];
|
|
}
|
|
|
|
if (_cachedControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]) {
|
|
[controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeElapsedText) ]];
|
|
}
|
|
|
|
if (_cachedControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]) {
|
|
[controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeScrubber) ]];
|
|
}
|
|
|
|
if (_cachedControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]) {
|
|
[controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeDurationText) ]];
|
|
}
|
|
|
|
if (_cachedControls[ @(ASVideoPlayerNodeControlTypeFullScreenButton) ]) {
|
|
[controls addObject:_cachedControls[ @(ASVideoPlayerNodeControlTypeFullScreenButton) ]];
|
|
}
|
|
|
|
return controls;
|
|
}
|
|
|
|
|
|
#pragma mark - Layout
|
|
|
|
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
|
|
{
|
|
CGSize maxSize = constrainedSize.max;
|
|
|
|
// Prevent crashes through if infinite width or height
|
|
if (isinf(maxSize.width) || isinf(maxSize.height)) {
|
|
ASDisplayNodeAssert(NO, @"Infinite width or height in ASVideoPlayerNode");
|
|
maxSize = CGSizeZero;
|
|
}
|
|
|
|
_videoNode.style.preferredSize = maxSize;
|
|
|
|
ASLayoutSpec *layoutSpec;
|
|
if (_delegateFlags.delegateLayoutSpecForControls) {
|
|
layoutSpec = [_delegate videoPlayerNodeLayoutSpec:self forControls:_cachedControls forMaximumSize:maxSize];
|
|
} else {
|
|
layoutSpec = [self defaultLayoutSpecThatFits:maxSize];
|
|
}
|
|
|
|
NSMutableArray *children = [[NSMutableArray alloc] init];
|
|
|
|
if (_spinnerNode) {
|
|
ASCenterLayoutSpec *centerLayoutSpec = [ASCenterLayoutSpec centerLayoutSpecWithCenteringOptions:ASCenterLayoutSpecCenteringXY sizingOptions:ASCenterLayoutSpecSizingOptionDefault child:_spinnerNode];
|
|
centerLayoutSpec.style.preferredSize = maxSize;
|
|
[children addObject:centerLayoutSpec];
|
|
}
|
|
|
|
ASOverlayLayoutSpec *overlaySpec = [ASOverlayLayoutSpec overlayLayoutSpecWithChild:_videoNode overlay:layoutSpec];
|
|
overlaySpec.style.preferredSize = maxSize;
|
|
[children addObject:overlaySpec];
|
|
|
|
return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:children];
|
|
}
|
|
|
|
- (ASLayoutSpec *)defaultLayoutSpecThatFits:(CGSize)maxSize
|
|
{
|
|
_scrubberNode.style.preferredSize = CGSizeMake(maxSize.width, 44.0);
|
|
|
|
ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
|
|
spacer.style.flexGrow = 1.0;
|
|
|
|
ASStackLayoutSpec *controlbarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
|
|
spacing:10.0
|
|
justifyContent:ASStackLayoutJustifyContentStart
|
|
alignItems:ASStackLayoutAlignItemsCenter
|
|
children: [self controlsForLayoutSpec] ];
|
|
controlbarSpec.style.alignSelf = ASStackLayoutAlignSelfStretch;
|
|
|
|
UIEdgeInsets insets = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0);
|
|
|
|
ASInsetLayoutSpec *controlbarInsetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:insets child:controlbarSpec];
|
|
|
|
controlbarInsetSpec.style.alignSelf = ASStackLayoutAlignSelfStretch;
|
|
|
|
ASStackLayoutSpec *mainVerticalStack = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical
|
|
spacing:0.0
|
|
justifyContent:ASStackLayoutJustifyContentStart
|
|
alignItems:ASStackLayoutAlignItemsStart
|
|
children:@[spacer,controlbarInsetSpec]];
|
|
|
|
return mainVerticalStack;
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
- (id<ASVideoPlayerNodeDelegate>)delegate
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
- (void)setDelegate:(id<ASVideoPlayerNodeDelegate>)delegate
|
|
{
|
|
if (delegate == _delegate) {
|
|
return;
|
|
}
|
|
|
|
_delegate = delegate;
|
|
|
|
if (_delegate == nil) {
|
|
memset(&_delegateFlags, 0, sizeof(_delegateFlags));
|
|
} else {
|
|
_delegateFlags.delegateNeededDefaultControls = [_delegate respondsToSelector:@selector(videoPlayerNodeNeededDefaultControls:)];
|
|
_delegateFlags.delegateCustomControls = [_delegate respondsToSelector:@selector(videoPlayerNodeCustomControls:)];
|
|
_delegateFlags.delegateSpinnerTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeSpinnerTint:)];
|
|
_delegateFlags.delegateSpinnerStyle = [_delegate respondsToSelector:@selector(videoPlayerNodeSpinnerStyle:)];
|
|
_delegateFlags.delegateScrubberMaximumTrackTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberMaximumTrackTint:)];
|
|
_delegateFlags.delegateScrubberMinimumTrackTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberMinimumTrackTint:)];
|
|
_delegateFlags.delegateScrubberThumbTintColor = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberThumbTint:)];
|
|
_delegateFlags.delegateScrubberThumbImage = [_delegate respondsToSelector:@selector(videoPlayerNodeScrubberThumbImage:)];
|
|
_delegateFlags.delegateTimeLabelAttributes = [_delegate respondsToSelector:@selector(videoPlayerNodeTimeLabelAttributes:timeLabelType:)];
|
|
_delegateFlags.delegateLayoutSpecForControls = [_delegate respondsToSelector:@selector(videoPlayerNodeLayoutSpec:forControls:forMaximumSize:)];
|
|
_delegateFlags.delegateVideoNodeDidPlayToTime = [_delegate respondsToSelector:@selector(videoPlayerNode:didPlayToTime:)];
|
|
_delegateFlags.delegateVideoNodeWillChangeState = [_delegate respondsToSelector:@selector(videoPlayerNode:willChangeVideoNodeState:toVideoNodeState:)];
|
|
_delegateFlags.delegateVideoNodePlaybackDidFinish = [_delegate respondsToSelector:@selector(videoPlayerNodeDidPlayToEnd:)];
|
|
_delegateFlags.delegateVideoNodeShouldChangeState = [_delegate respondsToSelector:@selector(videoPlayerNode:shouldChangeVideoNodeStateTo:)];
|
|
_delegateFlags.delegateTimeLabelAttributedString = [_delegate respondsToSelector:@selector(videoPlayerNode:timeStringForTimeLabelType:forTime:)];
|
|
_delegateFlags.delegatePlaybackButtonTint = [_delegate respondsToSelector:@selector(videoPlayerNodePlaybackButtonTint:)];
|
|
_delegateFlags.delegateFullScreenButtonImage = [_delegate respondsToSelector:@selector(videoPlayerNodeFullScreenButtonImage:)];
|
|
_delegateFlags.delegateDidTapVideoPlayerNode = [_delegate respondsToSelector:@selector(didTapVideoPlayerNode:)];
|
|
_delegateFlags.delegateDidTapFullScreenButtonNode = [_delegate respondsToSelector:@selector(didTapFullScreenButtonNode:)];
|
|
_delegateFlags.delegateVideoPlayerNodeDidSetCurrentItem = [_delegate respondsToSelector:@selector(videoPlayerNode:didSetCurrentItem:)];
|
|
_delegateFlags.delegateVideoPlayerNodeDidStallAtTimeInterval = [_delegate respondsToSelector:@selector(videoPlayerNode:didStallAtTimeInterval:)];
|
|
_delegateFlags.delegateVideoPlayerNodeDidStartInitialLoading = [_delegate respondsToSelector:@selector(videoPlayerNodeDidStartInitialLoading:)];
|
|
_delegateFlags.delegateVideoPlayerNodeDidFinishInitialLoading = [_delegate respondsToSelector:@selector(videoPlayerNodeDidFinishInitialLoading:)];
|
|
_delegateFlags.delegateVideoPlayerNodeDidRecoverFromStall = [_delegate respondsToSelector:@selector(videoPlayerNodeDidRecoverFromStall:)];
|
|
}
|
|
}
|
|
|
|
- (void)setControlsDisabled:(BOOL)controlsDisabled
|
|
{
|
|
if (_controlsDisabled == controlsDisabled) {
|
|
return;
|
|
}
|
|
|
|
_controlsDisabled = controlsDisabled;
|
|
|
|
if (_controlsDisabled && _cachedControls.count > 0) {
|
|
[self removeControls];
|
|
} else if (!_controlsDisabled) {
|
|
[self createControls];
|
|
}
|
|
}
|
|
|
|
- (void)setShouldAutoPlay:(BOOL)shouldAutoPlay
|
|
{
|
|
if (_shouldAutoPlay == shouldAutoPlay) {
|
|
return;
|
|
}
|
|
_shouldAutoPlay = shouldAutoPlay;
|
|
_videoNode.shouldAutoplay = _shouldAutoPlay;
|
|
}
|
|
|
|
- (void)setShouldAutoRepeat:(BOOL)shouldAutoRepeat
|
|
{
|
|
if (_shouldAutoRepeat == shouldAutoRepeat) {
|
|
return;
|
|
}
|
|
_shouldAutoRepeat = shouldAutoRepeat;
|
|
_videoNode.shouldAutorepeat = _shouldAutoRepeat;
|
|
}
|
|
|
|
- (void)setMuted:(BOOL)muted
|
|
{
|
|
if (_muted == muted) {
|
|
return;
|
|
}
|
|
_muted = muted;
|
|
_videoNode.muted = _muted;
|
|
}
|
|
|
|
- (void)setPeriodicTimeObserverTimescale:(int32_t)periodicTimeObserverTimescale
|
|
{
|
|
if (_periodicTimeObserverTimescale == periodicTimeObserverTimescale) {
|
|
return;
|
|
}
|
|
_periodicTimeObserverTimescale = periodicTimeObserverTimescale;
|
|
_videoNode.periodicTimeObserverTimescale = _periodicTimeObserverTimescale;
|
|
}
|
|
|
|
- (NSString *)gravity
|
|
{
|
|
if (_gravity == nil) {
|
|
_gravity = _videoNode.gravity;
|
|
}
|
|
return _gravity;
|
|
}
|
|
|
|
- (void)setGravity:(NSString *)gravity
|
|
{
|
|
if (_gravity == gravity) {
|
|
return;
|
|
}
|
|
_gravity = gravity;
|
|
_videoNode.gravity = _gravity;
|
|
}
|
|
|
|
- (ASVideoNodePlayerState)playerState
|
|
{
|
|
return _videoNode.playerState;
|
|
}
|
|
|
|
- (BOOL)shouldAggressivelyRecoverFromStall
|
|
{
|
|
return _videoNode.shouldAggressivelyRecoverFromStall;
|
|
}
|
|
|
|
- (void) setPlaceholderImageURL:(NSURL *)placeholderImageURL
|
|
{
|
|
_videoNode.URL = placeholderImageURL;
|
|
}
|
|
|
|
- (NSURL*) placeholderImageURL
|
|
{
|
|
return _videoNode.URL;
|
|
}
|
|
|
|
- (ASVideoNode*)videoNode
|
|
{
|
|
return _videoNode;
|
|
}
|
|
|
|
- (void)setShouldAggressivelyRecoverFromStall:(BOOL)shouldAggressivelyRecoverFromStall
|
|
{
|
|
if (_shouldAggressivelyRecoverFromStall == shouldAggressivelyRecoverFromStall) {
|
|
return;
|
|
}
|
|
_shouldAggressivelyRecoverFromStall = shouldAggressivelyRecoverFromStall;
|
|
_videoNode.shouldAggressivelyRecoverFromStall = _shouldAggressivelyRecoverFromStall;
|
|
}
|
|
|
|
#pragma mark - Helpers
|
|
- (NSString *)timeStringForCMTime:(CMTime)time forTimeLabelType:(ASVideoPlayerNodeControlType)type
|
|
{
|
|
if (!CMTIME_IS_VALID(time)) {
|
|
return @"00:00";
|
|
}
|
|
if (_delegateFlags.delegateTimeLabelAttributedString) {
|
|
return [_delegate videoPlayerNode:self timeStringForTimeLabelType:type forTime:time];
|
|
}
|
|
|
|
NSUInteger dTotalSeconds = CMTimeGetSeconds(time);
|
|
|
|
NSUInteger dHours = floor(dTotalSeconds / 3600);
|
|
NSUInteger dMinutes = floor(dTotalSeconds % 3600 / 60);
|
|
NSUInteger dSeconds = floor(dTotalSeconds % 3600 % 60);
|
|
|
|
NSString *videoDurationText;
|
|
if (dHours > 0) {
|
|
videoDurationText = [NSString stringWithFormat:@"%i:%02i:%02i", (int)dHours, (int)dMinutes, (int)dSeconds];
|
|
} else {
|
|
videoDurationText = [NSString stringWithFormat:@"%02i:%02i", (int)dMinutes, (int)dSeconds];
|
|
}
|
|
return videoDurationText;
|
|
}
|
|
|
|
@end
|
|
|
|
#endif // TARGET_OS_IOS
|
|
#endif
|
|
|
|
#endif
|