/* HBAVPlayer.m $
This file is part of the HandBrake source code.
Homepage: .
It may be used under the terms of the GNU General Public License. */
#import "HBAVPlayer.h"
@import AVFoundation;
@import HandBrakeKit;
static void *HBAVPlayerRateContext = &HBAVPlayerRateContext;
static void *HBAVPlayerItemStatusContext = &HBAVPlayerItemStatusContext;
typedef void (^HBRateObverser)(void);
typedef void (^HBPlayableObverser)(void);
@interface HBAVPlayerRateObserver : NSObject
@property (nonatomic) HBRateObverser block;
- (void)postNotification;
@end
@implementation HBAVPlayerRateObserver
- (void)postNotification;
{
self.block();
}
@end
@interface HBAVPlayer ()
@property (nonatomic, strong) AVAsset *movie;
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) NSMutableSet *rateObservers;
@property (nonatomic, strong) NSMutableSet *playableObservers;
@property (nonatomic, readwrite) BOOL playable;
@end
@implementation HBAVPlayer
- (instancetype)initWithURL:(NSURL *)url
{
self = [super init];
if (self)
{
_movie = [AVAsset assetWithURL:url];;
if (!_movie)
{
return nil;
}
_player = [[AVPlayer alloc] init];
_layer = [CALayer layer];
if (!_layer || !_player)
{
return nil;
}
_rateObservers = [NSMutableSet set];
_playableObservers = [NSMutableSet set];
[self addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew context:HBAVPlayerRateContext];
[self addObserver:self forKeyPath:@"player.currentItem.status" options:NSKeyValueObservingOptionNew context:HBAVPlayerItemStatusContext];
NSArray *assetKeysToLoadAndTest = @[@"playable"];
[_movie loadValuesAsynchronouslyForKeys:assetKeysToLoadAndTest completionHandler:^(void) {
// The asset invokes its completion handler on an arbitrary queue when loading is complete.
// Because we want to access our AVPlayer in our ensuing set-up, we must dispatch our handler to the main queue.
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self _setUpPlaybackOfAsset:_movie withKeys:assetKeysToLoadAndTest];
});
}];
}
return self;
}
- (void)dealloc
{
@try
{
[self removeObserver:self forKeyPath:@"player.rate"];
[self removeObserver:self forKeyPath:@"player.currentItem.status"];
}
@catch (NSException *exception) {}
}
- (void)_setUpPlaybackOfAsset:(AVAsset *)asset withKeys:(NSArray *)keys
{
// First test whether the values of each of the keys we need have been successfully loaded.
for (NSString *key in keys)
{
NSError *error = nil;
if ([asset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed)
{
self.playable = NO;
return;
}
if (!asset.isPlayable)
{
self.playable = NO;
return;
}
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:asset];
[self.player replaceCurrentItemWithPlayerItem:playerItem];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
playerLayer.frame = self.layer.bounds;
playerLayer.autoresizingMask = kCALayerWidthSizable | kCALayerHeightSizable;
[self.layer addSublayer:playerLayer];
}
}
- (void)setPlayable:(BOOL)playable
{
_playable = playable;
for (HBPlayableObverser block in self.playableObservers)
{
block();
}
[self.playableObservers removeAllObjects];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == HBAVPlayerItemStatusContext)
{
AVPlayerStatus status = [change[NSKeyValueChangeNewKey] integerValue];
switch (status)
{
case AVPlayerItemStatusUnknown:
break;
case AVPlayerItemStatusReadyToPlay:
self.playable = YES;
break;
case AVPlayerItemStatusFailed:
self.playable = YES;
break;
}
}
else if (context == HBAVPlayerRateContext)
{
for (HBAVPlayerRateObserver *observer in self.rateObservers)
{
[observer postNotification];
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
#pragma mark Public properties
- (NSArray *)tracksWithMediaType:(NSString *)mediaType
{
NSMutableArray *result = [NSMutableArray array];
NSArray *tracks = self.player.currentItem.tracks;
for (AVPlayerItemTrack *track in tracks)
{
AVAssetTrack *assetTrack = track.assetTrack;
if ([assetTrack.mediaType isEqualToString:mediaType])
{
NSString *name = [HBUtilities languageCodeForIso6392Code:assetTrack.languageCode];
HBPlayerTrack *playerTrack = [[HBPlayerTrack alloc] initWithTrackName:name object:@(assetTrack.trackID) enabled:track.isEnabled];
[result addObject:playerTrack];
}
}
return result;
}
@synthesize audioTracks = _audioTracks;
- (NSArray *)audioTracks
{
if (_audioTracks == nil)
{
_audioTracks = [self tracksWithMediaType:AVMediaTypeAudio];
}
return _audioTracks;
}
@synthesize subtitlesTracks = _subtitlesTracks;
- (NSArray *)subtitlesTracks
{
if (_subtitlesTracks == nil)
{
_subtitlesTracks = [self tracksWithMediaType:AVMediaTypeSubtitle];
}
return _subtitlesTracks;
}
- (void)_enableTrack:(HBPlayerTrack *)playerTrack mediaType:(NSString *)mediaType
{
NSArray *tracks = self.player.currentItem.tracks;
for (AVPlayerItemTrack *track in tracks)
{
if ([track.assetTrack.mediaType isEqualToString:mediaType])
{
if (track.assetTrack.trackID == [playerTrack.representedObject integerValue])
{
track.enabled = YES;
}
else
{
track.enabled = NO;
}
}
}
}
- (void)enableAudioTrack:(HBPlayerTrack *)playerTrack
{
for (HBPlayerTrack *track in self.audioTracks)
{
track.enabled = NO;
}
playerTrack.enabled = YES;
[self _enableTrack:playerTrack mediaType:AVMediaTypeAudio];
}
- (void)enableSubtitlesTrack:(HBPlayerTrack *)playerTrack
{
for (HBPlayerTrack *track in self.subtitlesTracks)
{
track.enabled = NO;
}
playerTrack.enabled = YES;
[self _enableTrack:playerTrack mediaType:AVMediaTypeSubtitle];
}
- (NSTimeInterval)duration
{
return CMTimeGetSeconds(self.movie.duration);
}
- (NSTimeInterval)currentTime
{
return CMTimeGetSeconds(self.player.currentTime);
}
- (void)setCurrentTime:(NSTimeInterval)value
{
[self.player seekToTime:CMTimeMake((int64_t)(value * 1000), 1000) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)setRate:(float)rate
{
self.player.rate = rate;
}
- (float)rate
{
return self.player.rate;
}
- (float)volume
{
return self.player.volume;
}
- (void)setVolume:(float)volume
{
self.player.volume = volume;
}
#pragma mark public methods
- (void)loadPlayableValueAsynchronouslyWithCompletionHandler:(nullable void (^)(void))handler;
{
if (self.playable)
{
handler();
}
else
{
[self.playableObservers addObject:handler];
}
}
- (id)addPeriodicTimeObserverUsingBlock:(void (^)(NSTimeInterval time))block
{
CMTime interval = CMTimeMake(100, 1000);
id observer = [self.player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
block(0);
}];
return observer;
}
- (void)removeTimeObserver:(id)observer
{
[self.player removeTimeObserver:observer];
}
- (id)addRateObserverUsingBlock:(void (^)(void))block
{
HBAVPlayerRateObserver *observer = [[HBAVPlayerRateObserver alloc] init];
observer.block = block;
[self.rateObservers addObject:observer];
return observer;
}
- (void)removeRateObserver:(id)observer
{
[self.rateObservers removeObject:observer];
}
- (void)play
{
[self.player play];
}
- (void)pause
{
[self.player pause];
}
- (void)gotoBeginning
{
[self.player seekToTime:kCMTimeZero toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)gotoEnd
{
[self.player seekToTime:self.player.currentItem.duration toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)stepForward
{
CMTime frameTime = CMTimeMakeWithSeconds(1.0/30.0, self.player.currentItem.duration.timescale);
CMTime forwardTime = CMTimeAdd([self.player.currentItem currentTime], frameTime);
[self.player seekToTime:forwardTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
- (void)stepBackward
{
CMTime frameTime = CMTimeMakeWithSeconds(-1.0/30.0, self.player.currentItem.duration.timescale);
CMTime backwardTime = CMTimeAdd([self.player.currentItem currentTime], frameTime);
[self.player seekToTime:backwardTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
@end