Sanjeev Rao
Sanjeev Rao

Reputation: 2297

Timeline Progress bar for AVPlayer

AVPlayer is fully customizable, unfortunately there are convenient methods in AVPlayer for showing the time line progress bar.

AVPlayer *player = [AVPlayer playerWithURL:URL];
AVPlayerLayer *playerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];[self.view.layer addSubLayer:playerLayer];

I have an progress bar that indicates the how video has been played, and how much remained just as like MPMoviePlayer.

So how to get the timeline of video from AVPlayer and how to update the progress bar

Suggest me.

Upvotes: 29

Views: 45311

Answers (9)

tBug
tBug

Reputation: 827

technically you don't need a timer for this one. Just add property

    private var isUserDragingSlider: Bool = false

Then you need to set up a slider target.

STEP 1

    self.timeSlider.addTarget(self, action: #selector(handleSliderChangeValue(slider:event:)), for: .allEvents)

and in func handleSliderChangeValue you need to add this:

STEP 2

    @objc
func handleSliderChangeValue(slider: UISlider, event: UIEvent) {
    if let touchEvent = event.allTouches?.first {
        switch touchEvent.phase {
        case .began:
            self.isUserDragingSlider = true
        case .ended:
            self.isUserDragingSlider = false
            self.updateplayerWithSliderChangeValue()
        default:
            break
        }
    }
}

and in the end, you need to update your player with the selected time from a slider.

STEP 3

    func updateplayerWithSliderChangeValue() {
    if let duration = player?.currentItem?.duration {
        let totalSeconds = CMTimeGetSeconds(duration)
        if !(totalSeconds.isNaN || totalSeconds.isInfinite) {
            let newCurrentTime: TimeInterval = Double(self.timeSlider.value) * CMTimeGetSeconds(duration)
            let seekToTime: CMTime = CMTimeMakeWithSeconds(newCurrentTime, preferredTimescale: 600)
            self.player?.seek(to: seekToTime)
            self.isUserDragingSlider.toggle()
        }
    }
}

And last thing, You need to update your code. Video is updating your slider.

STEP 4

func setupSliderValue(_ seconds: Float64) {
    guard !(seconds.isNaN || seconds.isInfinite) else {
        return
    }
    
    if !isUserDragingSlider {
        if let duration = self.player?.currentItem?.duration {
            let durationInSeconds = CMTimeGetSeconds(duration)
            self.timeSlider.value = Float(seconds / durationInSeconds)
        }
    }
}

So main problem here is that when we move the slider we have a conflict between updating the slider ( from video ) and updating the video with our slider change.

That is why the slider is not working well. When you block updates from video time to slider with isUserDragingSlider all is working fine.

Upvotes: 0

Artem
Artem

Reputation: 228

I know it's an old question, but someone may find it useful. It's only Swift version:

 //set the timer, which will update your progress bar. You can use whatever time interval you want

 private func setupProgressTimer() {
    timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true, block: { [weak self] (completion) in
        guard let self = self else { return }
        self.updateProgress()
    })
}

//update progression of video, based on it's own data

private func updateProgress() {
    guard let duration = player?.currentItem?.duration.seconds,
        let currentMoment = player?.currentItem?.currentTime().seconds else { return }

    progressBar.progress = Float(currentMoment / duration)
}

Upvotes: 7

Zaid Pathan
Zaid Pathan

Reputation: 16820

Swifty answer to get progress:

private func addPeriodicTimeObserver() {
        // Invoke callback every half second
        let interval = CMTime(seconds: 0.5,
                              preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        // Queue on which to invoke the callback
        let mainQueue = DispatchQueue.main
        // Add time observer
        self.playerController?.player?.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) { [weak self] time in
            let currentSeconds = CMTimeGetSeconds(time)
            guard let duration = self?.playerController?.player?.currentItem?.duration else { return }
            let totalSeconds = CMTimeGetSeconds(duration)
            let progress: Float = Float(currentSeconds/totalSeconds)
            print(progress)
        }
    }

Ref

Upvotes: 5

Tiago Mendes
Tiago Mendes

Reputation: 5146

I took the answers from the iOSPawan and Raphael and then adapted to my needs. So I have music and UIProgressView which is always in loop and when you go to the next screen and come back the the song and the bar continued where they were left.

Code:

@interface YourClassViewController (){

    NSObject * periodicPlayerTimeObserverHandle;
}
@property (nonatomic, strong) AVPlayer *player;
@property (nonatomic, strong) UIProgressView *progressView;

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];

    if(_player != nil && ![self isPlaying])
    {
        [self musicPlay];
    }
}


-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    if (_player != nil) {

        [self stopPlaying];
    }
}

//   ----------
//     PLAYER
//   ----------

-(BOOL) isPlaying
{
    return ([_player rate] > 0);
}

-(void) musicPlay
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playerItemDidReachEnd:)
                                                 name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:[_player currentItem]];

    __weak typeof(self) weakSelf = self;
    periodicPlayerTimeObserverHandle = [_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
                                                                             queue:NULL
                                                                        usingBlock:^(CMTime time){
                                                                            [weakSelf updateProgressBar];
                                                                        }];
    [_player play];
}


-(void) stopPlaying
{
    @try {

        if(periodicPlayerTimeObserverHandle != nil)
        {
            [_player removeTimeObserver:periodicPlayerTimeObserverHandle];
            periodicPlayerTimeObserverHandle = nil;
        }

        [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
        [_player pause];
    }
    @catch (NSException * __unused exception) {}
}


-(void) playPreviewSong:(NSURL *) previewSongURL
{
    [self configureAVPlayerAndPlay:previewSongURL];
}


-(void) configureAVPlayerAndPlay: (NSURL*) url {

    if(_player)
        [self stopPlaying];

    AVAsset *audioFileAsset = [AVURLAsset URLAssetWithURL:url options:nil];
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:audioFileAsset];
    _player = [AVPlayer playerWithPlayerItem:playerItem];
    [_player addObserver:self forKeyPath:@"status" options:0 context:nil];

    CRLPerformBlockOnMainThreadAfterDelay(^{
        NSError *loadErr;
        if([audioFileAsset statusOfValueForKey:@"playable" error:&loadErr] == AVKeyValueStatusLoading)
        {
            [audioFileAsset cancelLoading];
            [self stopPlaying];
            [self showNetworkError:NSLocalizedString(@"Could not play file",nil)];
        }
    }, NETWORK_REQUEST_TIMEOUT);
}


- (void)updateProgressBar
{

    double currentTime = CMTimeGetSeconds(_player.currentTime);
    if(currentTime <= 0.05){
        [_progressView setProgress:(float)(0.0) animated:NO];
        return;
    }

    if (isfinite(currentTime) && (currentTime > 0))
    {
        float maxValue = CMTimeGetSeconds(_player.currentItem.asset.duration);
        [_progressView setProgress:(float)(currentTime/maxValue) animated:YES];
    }
}


-(void) showNetworkError:(NSString*)errorMessage
{
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"No connection", nil) message:errorMessage preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
        // do nothing
    }]];

    [self presentViewController:alert animated:YES completion:nil];
}


- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    if (object == _player && [keyPath isEqualToString:@"status"]) {
        if (_player.status == AVPlayerStatusFailed) {
            [self showNetworkError:NSLocalizedString(@"Could not play file", nil)];
        } else if (_player.status == AVPlayerStatusReadyToPlay) {
            NSLog(@"AVPlayerStatusReadyToPlay");
            [TLAppAudioAccess setAudioAccess:TLAppAudioAccessType_Playback];
            [self musicPlay];

        } else if (_player.status == AVPlayerItemStatusUnknown) {
            NSLog(@"AVPlayerItemStatusUnknown");
        }
    }
}


- (void)playerItemDidReachEnd:(NSNotification *)notification {

    if ([notification.object isEqual:self.player.currentItem])
    {
        [self.player seekToTime:kCMTimeZero];
        [self.player play];
    }
}


-(void) dealloc{

    @try {
        [_player removeObserver:self forKeyPath:@"status"];
    }
    @catch (NSException * __unused exception) {}
    [self stopPlaying];
    _player = nil;
}

Upvotes: 0

Harman
Harman

Reputation: 446

In my case, the following code works Swift 3:

var timeObserver: Any?
override func viewDidLoad() {
    ........
    let interval = CMTime(seconds: 0.05, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
    timeObserver = avPlayer.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { elapsedTime in
            self.updateSlider(elapsedTime: elapsedTime)     
        })
}

func updateSlider(elapsedTime: CMTime) {
    let playerDuration = playerItemDuration()
    if CMTIME_IS_INVALID(playerDuration) {
        seekSlider.minimumValue = 0.0
        return
    }
    let duration = Float(CMTimeGetSeconds(playerDuration))
    if duration.isFinite && duration > 0 {
        seekSlider.minimumValue = 0.0
        seekSlider.maximumValue = duration
        let time = Float(CMTimeGetSeconds(elapsedTime))
        seekSlider.setValue(time, animated: true)  
    }
}

private func playerItemDuration() -> CMTime {
    let thePlayerItem = avPlayer.currentItem
    if thePlayerItem?.status == .readyToPlay {
        return thePlayerItem!.duration
    }
    return kCMTimeInvalid
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    avPlayer.removeTimeObserver(timeObserver!)   
}

Upvotes: 2

Raphael
Raphael

Reputation: 3886

Thanks to iOSPawan for the code! I simplified the code to the necessary lines. This might be more clear to understand the concept. Basically I have implemented it like this and it works fine.

Before starting the video:

__weak NSObject *weakSelf = self;    
[_player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1.0 / 60.0, NSEC_PER_SEC)
                                      queue:NULL
                                 usingBlock:^(CMTime time){
                                                [weakSelf updateProgressBar];
                                             }];

[_player play];

Then you need to have a method to update your progress bar:

- (void)updateProgressBar
{
    double duration = CMTimeGetSeconds(_playerItem.duration);
    double time = CMTimeGetSeconds(_player.currentTime);
    _progressView.progress = (CGFloat) (time / duration);
}

Upvotes: 24

amleszk
amleszk

Reputation: 6290

    let progressView = UIProgressView(progressViewStyle: UIProgressViewStyle.Bar)
    self.view.addSubview(progressView)
    progressView.constrainHeight("\(1.0/UIScreen.mainScreen().scale)")
    progressView.alignLeading("", trailing: "", toView: self.view)
    progressView.alignBottomEdgeWithView(self.view, predicate: "")
    player.addPeriodicTimeObserverForInterval(CMTimeMakeWithSeconds(1/30.0, Int32(NSEC_PER_SEC)), queue: nil) { time in
        let duration = CMTimeGetSeconds(playerItem.duration)
        progressView.progress = Float((CMTimeGetSeconds(time) / duration))
    }

Upvotes: 12

iOSPawan
iOSPawan

Reputation: 2894

Please use the below code which is from apple example code "AVPlayerDemo".

    double interval = .1f;  

    CMTime playerDuration = [self playerItemDuration]; // return player duration.
    if (CMTIME_IS_INVALID(playerDuration)) 
    {
        return;
    } 
    double duration = CMTimeGetSeconds(playerDuration);
    if (isfinite(duration))
    {
        CGFloat width = CGRectGetWidth([yourSlider bounds]);
        interval = 0.5f * duration / width;
    }

    /* Update the scrubber during normal playback. */
    timeObserver = [[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC) 
                                                          queue:NULL 
                                                     usingBlock:
                                                      ^(CMTime time) 
                                                      {
                                                          [self syncScrubber];
                                                      }] retain];


- (CMTime)playerItemDuration
{
    AVPlayerItem *thePlayerItem = [player currentItem];
    if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay)
    {        

        return([playerItem duration]);
    }

    return(kCMTimeInvalid);
}

And in syncScrubber method update the UISlider or UIProgressBar value.

- (void)syncScrubber
{
    CMTime playerDuration = [self playerItemDuration];
    if (CMTIME_IS_INVALID(playerDuration)) 
    {
        yourSlider.minimumValue = 0.0;
        return;
    } 

    double duration = CMTimeGetSeconds(playerDuration);
    if (isfinite(duration) && (duration > 0))
    {
        float minValue = [ yourSlider minimumValue];
        float maxValue = [ yourSlider maximumValue];
        double time = CMTimeGetSeconds([player currentTime]);
        [yourSlider setValue:(maxValue - minValue) * time / duration + minValue];
    }
} 

Upvotes: 31

Igor Bidiniuc
Igor Bidiniuc

Reputation: 1560

for timeline i do this

-(void)changeSliderValue {

double duration = CMTimeGetSeconds(self.player.currentItem.duration);

[lengthSlider setMaximumValue:(float)duration];

lengthSlider.value = CMTimeGetSeconds([self.player currentTime]);

int seconds = lengthSlider.value,minutes = seconds/60,hours = minutes/60;

int secondsRemain = lengthSlider.maximumValue - seconds,minutesRemain = secondsRemain/60,hoursRemain = minutesRemain/60;

seconds = seconds-minutes*60;

minutes = minutes-hours*60;

secondsRemain = secondsRemain - minutesRemain*60;

minutesRemain = minutesRemain - hoursRemain*60;

NSString *hourStr,*minuteStr,*secondStr,*hourStrRemain,*minuteStrRemain,*secondStrRemain;

hourStr = hours > 9 ? [NSString stringWithFormat:@"%d",hours] : [NSString stringWithFormat:@"0%d",hours];

minuteStr = minutes > 9 ? [NSString stringWithFormat:@"%d",minutes] : [NSString stringWithFormat:@"0%d",minutes];

secondStr = seconds > 9 ? [NSString stringWithFormat:@"%d",seconds] : [NSString stringWithFormat:@"0%d",seconds];

hourStrRemain = hoursRemain > 9 ? [NSString stringWithFormat:@"%d",hoursRemain] : [NSString stringWithFormat:@"0%d",hoursRemain];

minuteStrRemain = minutesRemain > 9 ? [NSString stringWithFormat:@"%d",minutesRemain] : [NSString stringWithFormat:@"0%d",minutesRemain];

secondStrRemain = secondsRemain > 9 ? [NSString stringWithFormat:@"%d",secondsRemain] : [NSString stringWithFormat:@"0%d",secondsRemain];

timePlayed.text = [NSString stringWithFormat:@"%@:%@:%@",hourStr,minuteStr,secondStr];

timeRemain.text = [NSString stringWithFormat:@"-%@:%@:%@",hourStrRemain,minuteStrRemain,secondStrRemain];

And import CoreMedia framework

lengthSlider is UISlider

Upvotes: 1

Related Questions