Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions ASBPlayerScrubbing/ASBPlayerScrubbing.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,7 @@
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>

@class ASBPlayerScrubbing;

@protocol ASBPlayerScrubbingDelegate <NSObject>

- (void)playerScrubbingDidUpdateTime:(ASBPlayerScrubbing *)scrubbing;

@end

#import "ASBPlayerScrubbingDelegate.h"

@interface ASBPlayerScrubbing : NSObject

Expand All @@ -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
221 changes: 134 additions & 87 deletions ASBPlayerScrubbing/ASBPlayerScrubbing.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -156,7 +139,7 @@ + (CGFloat)nominalFrameRateForPlayer:(AVPlayer *)player
return track.nominalFrameRate;
}

- (void)setupSliderTap
- (void)setupSlider
{
UITapGestureRecognizer *gesture;

Expand All @@ -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
Expand All @@ -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];
Expand All @@ -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))
{
Expand All @@ -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
Expand All @@ -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
Expand All @@ -270,50 +259,83 @@ - (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;
CGFloat ratio;
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;
Expand All @@ -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<NSString *,id> *)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
Loading