Reputation: 2297
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
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
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
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)
}
}
Upvotes: 5
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
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
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
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
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
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