From e53308f9342d911a9d3e8f4d1eeba6e53fabc0ab Mon Sep 17 00:00:00 2001 From: Damiano Galassi Date: Tue, 3 May 2016 18:47:14 +0200 Subject: MacGui: use AVFoundation as the first option for the preview playback. Add volume and tracks controls to the player hud. --- macosx/HBAVPlayer.m | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 macosx/HBAVPlayer.m (limited to 'macosx/HBAVPlayer.m') diff --git a/macosx/HBAVPlayer.m b/macosx/HBAVPlayer.m new file mode 100644 index 000000000..ef72734c2 --- /dev/null +++ b/macosx/HBAVPlayer.m @@ -0,0 +1,367 @@ +/* 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 -- cgit v1.2.3