AVPlayer的时间轴进度条

29

AVPlayer 可以进行全方位定制,但是不幸的是,AVPlayer 没有方便的方法来显示时间轴进度条。

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

我有一个进度条,用来指示视频已播放的进度和剩余的进度,就像 MPMoviePlayer 一样。

那么如何从 AVPlayer 获取视频的时间线,并更新进度条呢?

请给予建议。


1
请考虑使用AVPlayerViewController。它非常简单,可以进行播放(但可能不适合您的需求)。只是提醒您,以防您不知道它。(编辑)-糟糕,这是三年前的事情:P。 - Matej
9个回答

30
请使用以下代码,该代码来自苹果示例代码 "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);
}

在syncScrubber方法中,更新UISlider或UIProgressBar的值。

- (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];
    }
} 

变量timeObserver是用来做什么的? - Burhanuddin Sunelwala
你需要timeObserver来实际取消“观察”,并且只要你想观察,就需要保留它。你可以阅读苹果文档关于addPeriodicTimeObserverForInterval:queue:usingBlock:的内容。 - Nils Ziehn

24

感谢 iOSPawan 提供的代码!我对代码进行了简化,只保留了必要的行。这可能更容易理解该概念。基本上我是这样实现的,并且它可以正常工作。

开始播放视频之前:

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

[_player play];

那么你需要有一种方法来更新你的进度条:

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

你在这里误用了 __block。如果需要在块内对变量进行赋值,则使用 __block。你可能想要使用 __weak。 - kball
已经更新了你的代码。但是另外需要注意的是,你需要保留 addPeriodicTimeObserverForInterval: 方法返回的对象引用,这样才能继续发送消息并且在以后可以将其删除。参考链接:https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVPlayer_Class/#//apple_ref/occ/instm/AVPlayer/addPeriodicTimeObserverForInterval:queue:usingBlock: - kball
timeObserver应在停止播放后被移除。 返回一个符合NSObject协议的对象。只要您希望播放器调用时间观察者,就必须保留此返回值。 将此对象传递给-removeTimeObserver:以取消时间观察。 - Leo
这里原本的weakSelf对我并没有生效,但是这个可以: __weak typeof(self) weakSelf = self; - Tiago Mendes

12
    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))
    }

7

我知道这是一个老问题,但有人可能会发现它有用。这只是Swift版本:

 //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)
}

谢谢你,救了我一命。我花费了很多时间在添加观察者上,但还是不起作用。但是感谢你提供这个简单的答案。 - iPhone 7
有很多答案,但没有一个是准确、完整或过时的。你的答案是正确和更新的。再次感谢。 - iPhone 7
这是2021年最佳答案。 - Alex Hartford

5

获取进度的Swifty解决方案:

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


2
在我的情况下,以下代码可用于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!)   
}

1

对于时间轴,我会这样做

-(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];

导入CoreMedia框架

lengthSlider是UISlider


我正在从URL播放歌曲。但是在CMTime持续时间和CMTime当前时间,我得到了0。这里的问题是什么导致了0值?你知道解决方案吗? - Moxarth
这是关于此的文档:https://developer.apple.com/documentation/avfoundation/avplayeritem/1389386-duration - Igor Bidiniuc

0
我从iOSPawan和Raphael那里获取了答案,然后根据我的需求进行了调整。 因此,我有音乐和UIProgressView,它们始终处于循环状态,当您转到下一个屏幕并返回时,歌曲和进度条会继续播放。
代码:
@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;
}

0

从技术上讲,您不需要为此添加计时器。 只需添加属性即可

    private var isUserDragingSlider: Bool = false

接下来,您需要设置一个滑块目标。

步骤1

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

在函数handleSliderChangeValue中,您需要添加以下内容:

步骤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
        }
    }
}

最后,您需要使用滑块选择的时间更新播放器。
第三步
    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()
        }
    }
}

最后一件事,您需要更新您的代码。视频正在更新您的滑块。
第四步
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)
        }
    }
}

这里的主要问题是,当我们移动滑块时,更新滑块(来自视频)和使用滑块更改更新视频之间存在冲突。

这就是为什么滑块不能正常工作的原因。当您使用isUserDragingSlider阻止从视频时间到滑块的更新时,一切都正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接