mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-09-08 21:49:41 +00:00
609 lines
16 KiB
Plaintext
609 lines
16 KiB
Plaintext
/* Copyright (c) 2014-present, Facebook, Inc.
|
|
* All rights reserved.
|
|
*
|
|
* This source code is licensed under the BSD-style license found in the
|
|
* LICENSE file in the root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "ASVideoNode.h"
|
|
#import "ASDefaultPlayButton.h"
|
|
|
|
static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) {
|
|
return ASObjectIsEqual(asset1, asset2)
|
|
|| ([asset1 isKindOfClass:[AVURLAsset class]]
|
|
&& [asset2 isKindOfClass:[AVURLAsset class]]
|
|
&& ASObjectIsEqual(((AVURLAsset *)asset1).URL, ((AVURLAsset *)asset2).URL));
|
|
}
|
|
|
|
static UIViewContentMode ASContentModeFromVideoGravity(NSString *videoGravity) {
|
|
if ([videoGravity isEqualToString:AVLayerVideoGravityResizeAspect]) {
|
|
return UIViewContentModeScaleAspectFit;
|
|
} else if ([videoGravity isEqual:AVLayerVideoGravityResizeAspectFill]) {
|
|
return UIViewContentModeScaleAspectFill;
|
|
} else {
|
|
return UIViewContentModeScaleToFill;
|
|
}
|
|
}
|
|
|
|
@interface ASVideoNode ()
|
|
{
|
|
ASDN::RecursiveMutex _videoLock;
|
|
|
|
__weak id<ASVideoNodeDelegate> _delegate;
|
|
|
|
BOOL _shouldBePlaying;
|
|
|
|
BOOL _shouldAutorepeat;
|
|
BOOL _shouldAutoplay;
|
|
|
|
BOOL _muted;
|
|
|
|
AVAsset *_asset;
|
|
|
|
AVPlayerItem *_currentPlayerItem;
|
|
AVPlayer *_player;
|
|
|
|
ASImageNode *_placeholderImageNode; // TODO: Make ASVideoNode an ASImageNode subclass; remove this.
|
|
|
|
ASButtonNode *_playButton;
|
|
ASDisplayNode *_playerNode;
|
|
ASDisplayNode *_spinner;
|
|
NSString *_gravity;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation ASVideoNode
|
|
|
|
// TODO: Support preview images with HTTP Live Streaming videos.
|
|
|
|
#pragma mark - Construction and Layout
|
|
|
|
- (instancetype)init
|
|
{
|
|
if (!(self = [super init])) {
|
|
return nil;
|
|
}
|
|
|
|
self.playButton = [[ASDefaultPlayButton alloc] init];
|
|
self.gravity = AVLayerVideoGravityResizeAspect;
|
|
[self addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock
|
|
{
|
|
ASDisplayNodeAssertNotSupported();
|
|
return nil;
|
|
}
|
|
|
|
- (ASDisplayNode *)constructPlayerNode
|
|
{
|
|
ASDisplayNode * playerNode = [[ASDisplayNode alloc] initWithLayerBlock:^CALayer *{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
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
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
ASDisplayNodeAssert(_asset, @"ASVideoNode must be initialized with an AVAsset");
|
|
[self removePlayerItemObservers];
|
|
|
|
if (_asset) {
|
|
if ([_asset.tracks count]) {
|
|
_currentPlayerItem = [[AVPlayerItem alloc] initWithAsset:_asset];
|
|
} else {
|
|
_currentPlayerItem = [[AVPlayerItem alloc] initWithURL:((AVURLAsset *)_asset).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];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
}
|
|
}
|
|
|
|
- (void)removePlayerItemObservers
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
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];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
|
|
}
|
|
}
|
|
|
|
- (void)didLoad
|
|
{
|
|
[super didLoad];
|
|
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (_shouldBePlaying) {
|
|
_playerNode = [self constructPlayerNode];
|
|
[self insertSubnode:_playerNode atIndex:0];
|
|
} else if (_asset) {
|
|
[self setPlaceholderImagefromAsset:_asset];
|
|
}
|
|
}
|
|
|
|
- (void)layout
|
|
{
|
|
[super layout];
|
|
|
|
CGRect bounds = self.bounds;
|
|
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
_placeholderImageNode.frame = bounds;
|
|
_playerNode.frame = bounds;
|
|
_playButton.frame = bounds;
|
|
|
|
CGFloat horizontalDiff = (bounds.size.width - _playButton.bounds.size.width)/2;
|
|
CGFloat verticalDiff = (bounds.size.height - _playButton.bounds.size.height)/2;
|
|
_playButton.hitTestSlop = UIEdgeInsetsMake(-verticalDiff, -horizontalDiff, -verticalDiff, -horizontalDiff);
|
|
|
|
_spinner.bounds = CGRectMake(0, 0, 44, 44);
|
|
_spinner.position = CGPointMake(bounds.size.width/2, bounds.size.height/2);
|
|
}
|
|
|
|
- (void)setPlaceholderImagefromAsset:(AVAsset*)asset
|
|
{
|
|
ASPerformBlockOnBackgroundThread(^{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
AVAssetImageGenerator *imageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:_asset];
|
|
imageGenerator.appliesPreferredTrackTransform = YES;
|
|
NSArray *times = @[[NSValue valueWithCMTime:CMTimeMake(0, 1)]];
|
|
|
|
[imageGenerator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
|
|
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
// 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) {
|
|
[self setPlaceholderImage:[UIImage imageWithCGImage:image]];
|
|
}
|
|
}];
|
|
});
|
|
}
|
|
|
|
- (void)setPlaceholderImage:(UIImage *)image
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (_placeholderImageNode == nil) {
|
|
_placeholderImageNode = [[ASImageNode alloc] init];
|
|
_placeholderImageNode.layerBacked = YES;
|
|
_placeholderImageNode.contentMode = ASContentModeFromVideoGravity(_gravity);
|
|
}
|
|
|
|
_placeholderImageNode.image = image;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
[self insertSubnode:_placeholderImageNode atIndex:0];
|
|
[self setNeedsLayout];
|
|
});
|
|
}
|
|
|
|
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState
|
|
{
|
|
[super interfaceStateDidChange:newState fromState:oldState];
|
|
|
|
BOOL nowVisible = ASInterfaceStateIncludesVisible(newState);
|
|
BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState);
|
|
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
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
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
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.image == nil) {
|
|
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
|
|
{
|
|
if (self.delegate && [self.delegate respondsToSelector:@selector(videoNodeWasTapped:)]) {
|
|
[self.delegate videoNodeWasTapped:self];
|
|
} else {
|
|
if (_shouldBePlaying) {
|
|
[self pause];
|
|
} else {
|
|
[self play];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)fetchData
|
|
{
|
|
[super fetchData];
|
|
|
|
@try {
|
|
[_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))];
|
|
}
|
|
@catch (NSException * __unused exception) {
|
|
NSLog(@"unnecessary removal in fetch data");
|
|
}
|
|
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
[self constructCurrentPlayerItemFromInitData];
|
|
[_currentPlayerItem addObserver:self forKeyPath:NSStringFromSelector(@selector(status)) options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
|
|
|
|
if (_player) {
|
|
[_player replaceCurrentItemWithPlayerItem:_currentPlayerItem];
|
|
} else {
|
|
_player = [[AVPlayer alloc] initWithPlayerItem:_currentPlayerItem];
|
|
_player.muted = _muted;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)clearFetchedData
|
|
{
|
|
[super clearFetchedData];
|
|
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
((AVPlayerLayer *)_playerNode.layer).player = nil;
|
|
_player = nil;
|
|
}
|
|
}
|
|
|
|
- (void)visibilityDidChange:(BOOL)isVisible
|
|
{
|
|
[super visibilityDidChange:isVisible];
|
|
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (_shouldAutoplay && _playerNode.isNodeLoaded) {
|
|
[self play];
|
|
} else if (_shouldAutoplay) {
|
|
_shouldBePlaying = YES;
|
|
}
|
|
if (isVisible) {
|
|
if (_playerNode.isNodeLoaded) {
|
|
if (!_player) {
|
|
[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
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
[_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
[_playButton removeFromSupernode];
|
|
|
|
_playButton = playButton;
|
|
|
|
[self addSubnode:playButton];
|
|
|
|
[_playButton addTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
}
|
|
|
|
- (ASButtonNode *)playButton
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
return _playButton;
|
|
}
|
|
|
|
- (void)setAsset:(AVAsset *)asset
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (ASAssetIsEqual(asset, _asset)) {
|
|
return;
|
|
}
|
|
|
|
_asset = asset;
|
|
|
|
// FIXME: Adopt -setNeedsFetchData when it is available
|
|
if (self.interfaceState & ASInterfaceStateFetchData) {
|
|
[self fetchData];
|
|
}
|
|
}
|
|
|
|
- (AVAsset *)asset
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _asset;
|
|
}
|
|
|
|
- (AVPlayer *)player
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _player;
|
|
}
|
|
|
|
- (void)setGravity:(NSString *)gravity
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
if (_playerNode.isNodeLoaded) {
|
|
((AVPlayerLayer *)_playerNode.layer).videoGravity = gravity;
|
|
}
|
|
_placeholderImageNode.contentMode = ASContentModeFromVideoGravity(gravity);
|
|
_gravity = gravity;
|
|
}
|
|
|
|
- (NSString *)gravity
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
return _gravity;
|
|
}
|
|
|
|
- (BOOL)muted
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
return _muted;
|
|
}
|
|
|
|
- (void)setMuted:(BOOL)muted
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
_player.muted = muted;
|
|
_muted = muted;
|
|
}
|
|
|
|
#pragma mark - Video Playback
|
|
|
|
- (void)play
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (!_spinner) {
|
|
_spinner = [[ASDisplayNode alloc] initWithViewBlock:^UIView *{
|
|
UIActivityIndicatorView *spinnnerView = [[UIActivityIndicatorView alloc] init];
|
|
spinnnerView.color = [UIColor whiteColor];
|
|
|
|
return spinnnerView;
|
|
}];
|
|
}
|
|
|
|
if (!_playerNode) {
|
|
_playerNode = [self constructPlayerNode];
|
|
|
|
if ([self.subnodes containsObject:_playButton]) {
|
|
[self insertSubnode:_playerNode belowSubnode:_playButton];
|
|
} else {
|
|
[self addSubnode:_playerNode];
|
|
}
|
|
}
|
|
|
|
[_player play];
|
|
_shouldBePlaying = YES;
|
|
|
|
[UIView animateWithDuration:0.15 animations:^{
|
|
_playButton.alpha = 0.0;
|
|
}];
|
|
|
|
if (![self ready] && _shouldBePlaying && ASInterfaceStateIncludesVisible(self.interfaceState)) {
|
|
[self addSubnode:_spinner];
|
|
[(UIActivityIndicatorView *)_spinner.view startAnimating];
|
|
}
|
|
}
|
|
|
|
- (BOOL)ready
|
|
{
|
|
return _currentPlayerItem.status == AVPlayerItemStatusReadyToPlay;
|
|
}
|
|
|
|
- (void)pause
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
[_player pause];
|
|
[((UIActivityIndicatorView *)_spinner.view) stopAnimating];
|
|
_shouldBePlaying = NO;
|
|
[UIView animateWithDuration:0.15 animations:^{
|
|
_playButton.alpha = 1.0;
|
|
}];
|
|
}
|
|
|
|
- (BOOL)isPlaying
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
return (_player.rate > 0 && !_player.error);
|
|
}
|
|
|
|
|
|
#pragma mark - Playback observers
|
|
|
|
- (void)didPlayToEnd:(NSNotification *)notification
|
|
{
|
|
if ([_delegate respondsToSelector:@selector(videoPlaybackDidFinish:)]) {
|
|
[_delegate videoPlaybackDidFinish:self];
|
|
}
|
|
[_player seekToTime:kCMTimeZero];
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)willEnterForeground:(NSNotification *)notification
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (_shouldBePlaying) {
|
|
[self play];
|
|
}
|
|
}
|
|
|
|
- (void)didEnterBackground:(NSNotification *)notification
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
|
|
if (_shouldBePlaying) {
|
|
[self pause];
|
|
_shouldBePlaying = YES;
|
|
}
|
|
}
|
|
|
|
#pragma mark - Property Accessors for Tests
|
|
|
|
- (ASDisplayNode *)spinner
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _spinner;
|
|
}
|
|
|
|
- (ASImageNode *)placeholderImageNode
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _placeholderImageNode;
|
|
}
|
|
|
|
- (AVPlayerItem *)currentItem
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _currentPlayerItem;
|
|
}
|
|
|
|
- (void)setCurrentItem:(AVPlayerItem *)currentItem
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
_currentPlayerItem = currentItem;
|
|
}
|
|
|
|
- (ASDisplayNode *)playerNode
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _playerNode;
|
|
}
|
|
|
|
- (void)setPlayerNode:(ASDisplayNode *)playerNode
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
_playerNode = playerNode;
|
|
}
|
|
|
|
- (void)setPlayer:(AVPlayer *)player
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
_player = player;
|
|
}
|
|
|
|
- (BOOL)shouldBePlaying
|
|
{
|
|
ASDN::MutexLocker l(_videoLock);
|
|
return _shouldBePlaying;
|
|
}
|
|
|
|
#pragma mark - Lifecycle
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_playButton removeTarget:self action:@selector(tapped) forControlEvents:ASControlNodeEventTouchUpInside];
|
|
[self removePlayerItemObservers];
|
|
|
|
@try {
|
|
[_currentPlayerItem removeObserver:self forKeyPath:NSStringFromSelector(@selector(status))];
|
|
}
|
|
@catch (NSException * __unused exception) {
|
|
NSLog(@"unnecessary removal in dealloc");
|
|
}
|
|
}
|
|
|
|
@end
|