diff --git a/ASBPlayerScrubbing/ASBPlayerScrubbing.h b/ASBPlayerScrubbing/ASBPlayerScrubbing.h index e994538..a550769 100644 --- a/ASBPlayerScrubbing/ASBPlayerScrubbing.h +++ b/ASBPlayerScrubbing/ASBPlayerScrubbing.h @@ -9,14 +9,7 @@ #import #import -@class ASBPlayerScrubbing; - -@protocol ASBPlayerScrubbingDelegate - -- (void)playerScrubbingDidUpdateTime:(ASBPlayerScrubbing *)scrubbing; - -@end - +#import "ASBPlayerScrubbingDelegate.h" @interface ASBPlayerScrubbing : NSObject @@ -38,4 +31,12 @@ // Returns the formatted representation of the specified time. If showTimeFrames is YES, the representation respects the player frame rate. - (NSString *)timecodeForTimeInterval:(NSTimeInterval)time; +@property (nonatomic, readonly) BOOL isPlaying; + +- (IBAction)playPause:(id)sender; +- (IBAction)play:(id)sender; +- (IBAction)pause:(id)sender; + ++ (NSString *)timecodeForTimeInterval:(NSTimeInterval)time frameRate:(CGFloat)frameRate showFrames:(BOOL)showFrames showHours:(BOOL)showHours; + @end diff --git a/ASBPlayerScrubbing/ASBPlayerScrubbing.m b/ASBPlayerScrubbing/ASBPlayerScrubbing.m index f3cc573..99ead0f 100644 --- a/ASBPlayerScrubbing/ASBPlayerScrubbing.m +++ b/ASBPlayerScrubbing/ASBPlayerScrubbing.m @@ -10,6 +10,8 @@ @interface ASBPlayerScrubbing () +@property (nonatomic, strong) AVPlayerItem *currentPlayerItem; + @property (nonatomic, assign) BOOL playAfterDrag; @property (nonatomic, assign) id timeObserver; @property (nonatomic, assign) CGFloat frameDuration; @@ -42,28 +44,27 @@ - (void)setup - (void)setSlider:(UISlider *)slider { _slider = slider; - [self setupSliderTap]; + [self setupSlider]; } - (void)setPlayer:(AVPlayer *)player { - CMTime duration; - [self.player pause]; [self removeTimeObserver]; _player = player; self.nbFramesPerSecond = [ASBPlayerScrubbing nominalFrameRateForPlayer:self.player]; - self.frameDuration = 1/self.nbFramesPerSecond; + + if (self.nbFramesPerSecond > 0) { + self.frameDuration = 1/self.nbFramesPerSecond; + } else { + self.frameDuration = 1/5.0; + } [self setupTimeObserver]; [self updateCurrentTimeLabelWithTime:0]; - duration = self.player.currentItem.duration; - if(!CMTIME_IS_VALID(duration) || CMTIME_IS_INDEFINITE(duration)) - { - [player.currentItem addObserver:self forKeyPath:@"duration" options:NSKeyValueObservingOptionNew context:nil]; - } + [self addStatusObserverOnItem:self.player.currentItem]; } - (void)setShowMinusSignOnRemainingTime:(BOOL)showMinusSignOnRemainingTime @@ -95,47 +96,29 @@ - (void)setShowTimeHours:(BOOL)showTimeHours - (NSString *)timecodeForTimeInterval:(NSTimeInterval)time { - NSInteger seconds; - NSInteger hours; - NSInteger minutes; - CGFloat milliseconds; - NSInteger nbFrames = 0; - NSString *timecode; - NSString *sign; + return [ASBPlayerScrubbing timecodeForTimeInterval:time frameRate:self.nbFramesPerSecond showFrames:self.showTimeFrames showHours:self.showTimeHours]; +} + ++ (NSString *)timecodeForTimeInterval:(NSTimeInterval)time frameRate:(CGFloat)frameRate showFrames:(BOOL)showFrames showHours:(BOOL)showHours { + NSTimeInterval frametime = time - floorf(time); + int frames = frametime * frameRate; - sign = ((time < 0) && self.showMinusSignOnRemainingTime?@"\u2212":@""); - time = ABS(time); - hours = time/60/24; - minutes = (time - hours*24)/60; - seconds = (time - hours*24) - minutes*60; + int totalSeconds = (int)ceilf(time); - if(self.showTimeFrames) - { - milliseconds = time - (NSInteger)time; - nbFrames = milliseconds*self.nbFramesPerSecond; + int seconds = totalSeconds % 60; + int minutes = (totalSeconds / 60) % 60; + int hours = totalSeconds / 3600; + + NSString *timecode = @""; + + if (showFrames) { + timecode = [NSString stringWithFormat:@":%02d", frames]; } - if((hours > 0) || self.showTimeHours) - { - if(self.showTimeFrames) - { - timecode = [NSString stringWithFormat:@"%@%d:%02d:%02d:%02d", sign, (int)hours, (int)minutes, (int)seconds, (int)nbFrames]; - } - else - { - timecode = [NSString stringWithFormat:@"%@%d:%02d:%02d", sign, (int)hours, (int)minutes, (int)seconds]; - } - } - else - { - if(self.showTimeFrames) - { - timecode = [NSString stringWithFormat:@"%@%02d:%02d:%02d", sign, (int)minutes, (int)seconds, (int)nbFrames]; - } - else - { - timecode = [NSString stringWithFormat:@"%@%02d:%02d", sign, (int)minutes, (int)seconds]; - } + if (hours > 0 || showHours) { + timecode = [NSString stringWithFormat:@"%2d:%02d:%02d%@", hours, minutes, seconds, timecode]; + } else { + timecode = [NSString stringWithFormat:@"%02d:%02d%@", minutes, seconds, timecode]; } return timecode; @@ -147,7 +130,7 @@ + (CGFloat)nominalFrameRateForPlayer:(AVPlayer *)player AVAssetTrack *track = nil; NSArray *tracks; - tracks = [player.currentItem.asset tracksWithMediaType:AVMediaTypeVideo]; + tracks = player.currentItem.asset.tracks; if(tracks.count > 0) { track = tracks[0]; @@ -156,7 +139,7 @@ + (CGFloat)nominalFrameRateForPlayer:(AVPlayer *)player return track.nominalFrameRate; } -- (void)setupSliderTap +- (void)setupSlider { UITapGestureRecognizer *gesture; @@ -165,6 +148,11 @@ - (void)setupSliderTap gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSliderTap:)]; [self.slider addGestureRecognizer:gesture]; + + [self.slider addTarget:self action:@selector(sliderValueChanged:forEvent:) forControlEvents:UIControlEventValueChanged]; + [self.slider addTarget:self action:@selector(sliderDidEndDragging:forEvent:) forControlEvents:UIControlEventTouchUpInside]; + [self.slider addTarget:self action:@selector(sliderDidCancelDragging:forEvent:) forControlEvents:UIControlEventTouchUpOutside]; + [self.slider addTarget:self action:@selector(sliderDidCancelDragging:forEvent:) forControlEvents:UIControlEventTouchCancel]; } - (void)removeTimeObserver @@ -182,11 +170,11 @@ - (void)setupTimeObserver if(self.timeObserver != nil) return; - + weakSelf = self; - if(self.nbFramesPerSecond > 0) + if(self.frameDuration > 0) { - self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1/self.nbFramesPerSecond, NSEC_PER_SEC) + self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(self.frameDuration, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) { [weakSelf playerTimeChanged]; @@ -203,6 +191,12 @@ - (void)playerTimeChanged if(self.player.currentItem == nil) return; + if (CMTIME_COMPARE_INLINE(self.player.currentItem.currentTime, ==, self.player.currentItem.duration)) { + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(playerScrubbingDidFinishPlaying:)]) { + [self.delegate playerScrubbingDidFinishPlaying:self]; + } + } + nbSecondsElapsed = CMTimeGetSeconds(self.player.currentItem.currentTime); if(CMTIME_IS_VALID(self.player.currentItem.duration) && !CMTIME_IS_INDEFINITE(self.player.currentItem.duration)) { @@ -219,7 +213,9 @@ - (void)playerTimeChanged [self updateCurrentTimeLabelWithTime:nbSecondsElapsed]; [self updateRemainingTimeLabelWithTime:nbSecondsDuration - nbSecondsElapsed]; - [self.delegate playerScrubbingDidUpdateTime:self]; + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(playerScrubbingDidUpdateTime:)]) { + [self.delegate playerScrubbingDidUpdateTime:self]; + } } - (void)updateDurationLabelWithTime:(NSTimeInterval)time @@ -246,22 +242,15 @@ - (void)updateRemainingTimeLabelWithTime:(NSTimeInterval)time self.remainingTimeLabel.text = [self timecodeForTimeInterval:-time]; } -- (void)updatePlayer:(BOOL)playIfNeeded +- (void)updatePlayer { CGFloat nbSecondsDuration; CMTime time; nbSecondsDuration = CMTimeGetSeconds(self.player.currentItem.duration); - time = CMTimeMakeWithSeconds(nbSecondsDuration*self.slider.value, NSEC_PER_SEC); - [self.player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { - if(playIfNeeded && (self.slider.value < self.slider.maximumValue)) - { - if(self.playAfterDrag) - { - [self.player play]; - } - } - }]; + int timescale = self.player.currentItem.asset.duration.timescale; + time = CMTimeMakeWithSeconds(nbSecondsDuration*self.slider.value, timescale); + [self.player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {}]; } #pragma mark - Actions @@ -270,36 +259,71 @@ - (IBAction)sliderValueChanged:(id)sender forEvent:(UIEvent *)event UITouch *touch; touch = [[event allTouches] anyObject]; - if(touch.phase == UITouchPhaseBegan) + + if([self isPlaying] && touch.phase == UITouchPhaseBegan) { - self.playAfterDrag = (self.player.rate > 0); + self.playAfterDrag = [self isPlaying]; [self.player pause]; } - [self updatePlayer:(touch.phase == UITouchPhaseEnded)]; + [self updatePlayer]; +} + +- (IBAction)sliderDidEndDragging:(id)sender forEvent:(UIEvent *)event +{ + if(self.playAfterDrag) + { + self.playAfterDrag = NO; + [self.player play]; + } +} + +- (IBAction)sliderDidCancelDragging:(id)sender forEvent:(UIEvent *)event +{ + if(self.playAfterDrag) + { + self.playAfterDrag = NO; + [self.player play]; + } +} + +- (BOOL)isPlaying +{ + return !(self.player.rate == 0); } - (IBAction)playPause:(id)sender { - if(self.player.rate == 0) + if(self.isPlaying) { - if(CMTIME_COMPARE_INLINE(self.player.currentTime, == , self.player.currentItem.duration)) - { - [self.player seekToTime:kCMTimeZero completionHandler:^(BOOL finished) { - [self.player play]; - }]; - } - else - { + [self pause:sender]; + } + else + { + + [self play:sender]; + } +} + +- (IBAction)play:(id)sender +{ + if(CMTIME_COMPARE_INLINE(self.player.currentTime, == , self.player.currentItem.duration)) + { + [self.player seekToTime:kCMTimeZero completionHandler:^(BOOL finished) { [self.player play]; - } + }]; } else { - [self.player pause]; + [self.player play]; } } +- (IBAction)pause:(id)sender +{ + [self.player pause]; +} + - (void)handleSliderTap:(UIGestureRecognizer *)gesture { CGPoint point; @@ -307,13 +331,11 @@ - (void)handleSliderTap:(UIGestureRecognizer *)gesture CGFloat delta; CGFloat value; CGFloat thumbWidth; - BOOL isPlaying; // tap on thumb, let slider deal with it if (self.slider.highlighted) return; - isPlaying = (self.player.rate > 0); CGRect trackRect = [self.slider trackRectForBounds:self.slider.bounds]; CGRect thumbRect = [self.slider thumbRectForBounds:self.slider.bounds trackRect:trackRect value:0]; CGSize thumbSize = thumbRect.size; @@ -334,16 +356,41 @@ - (void)handleSliderTap:(UIGestureRecognizer *)gesture delta = ratio * (self.slider.maximumValue - self.slider.minimumValue); value = self.slider.minimumValue + delta; [self.slider setValue:value animated:YES]; - [self updatePlayer:isPlaying]; + [self updatePlayer]; } -#pragma mark - KVO -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - if(CMTIME_IS_VALID(self.player.currentItem.duration) && !CMTIME_IS_INDEFINITE(self.player.currentItem.duration)) - { - [self.player.currentItem removeObserver:self forKeyPath:@"duration"]; - [self playerTimeChanged]; +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (object == self.player.currentItem && [keyPath isEqualToString:@"status"]) { + AVPlayerItem *item = (AVPlayerItem *)object; + switch (item.status) { + case AVPlayerItemStatusFailed: + if (self.delegate != nil && [self.delegate respondsToSelector:@selector(playerScrubbingDidError:)]) { + [self.delegate playerScrubbingDidError:self]; + } + break; + case AVPlayerItemStatusReadyToPlay: + case AVPlayerItemStatusUnknown: + break; + } } } + +- (void)setCurrentPlayerItem:(AVPlayerItem *)currentPlayerItem { + [self removeStatusObserverOnItem:_currentPlayerItem]; + _currentPlayerItem = currentPlayerItem; +} + +- (void)addStatusObserverOnItem:(AVPlayerItem *)item { + self.currentPlayerItem = self.player.currentItem; + [self.currentPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:NULL]; +} + +- (void)removeStatusObserverOnItem:(AVPlayerItem *)item { + [item removeObserver:self forKeyPath:@"status"]; +} + +- (void)dealloc { + [self removeStatusObserverOnItem:self.currentPlayerItem]; +} + @end diff --git a/ASBPlayerScrubbing/ASBPlayerScrubbingDelegate.h b/ASBPlayerScrubbing/ASBPlayerScrubbingDelegate.h new file mode 100644 index 0000000..0fe721d --- /dev/null +++ b/ASBPlayerScrubbing/ASBPlayerScrubbingDelegate.h @@ -0,0 +1,19 @@ +// +// ASBPlayerScrubbingDelegate.h +// WildcardApp +// +// Created by Lacy Rhoades on 9/28/15. +// Copyright © 2015 Doug Petkanics. All rights reserved. +// + +#import + +@class ASBPlayerScrubbing; + +@protocol ASBPlayerScrubbingDelegate + +@optional +- (void)playerScrubbingDidUpdateTime:(ASBPlayerScrubbing *)scrubbing; +- (void)playerScrubbingDidFinishPlaying:(ASBPlayerScrubbing *)scrubbing; +- (void)playerScrubbingDidError:(ASBPlayerScrubbing *)scrubbing; +@end