【问题标题】:AVPlayer stops playing and doesn't resume againAVPlayer 停止播放并且不再恢复
【发布时间】:2013-10-17 23:15:51
【问题描述】:

在我的应用程序中,我必须播放存储在网络服务器上的音频文件。我正在使用AVPlayer。我拥有所有播放/暂停控件以及所有代表和观察者,它们工作得非常好。在播放小音频文件时,一切都很好。

当播放长音频文件时,它也开始正常播放,但几秒钟后AVPlayer 暂停播放(很可能是为了缓冲它)。问题是它不会再次自行恢复。它保持暂停状态,如果我再次手动按下播放按钮,它会再次流畅播放。

我想知道为什么AVPlayer 不会自动恢复,我怎样才能在不用户再次按下播放按钮的情况下再次恢复音频?谢谢。

【问题讨论】:

标签: ios audio avfoundation avplayer


【解决方案1】:

是的,它会停止,因为缓冲区是空的,所以它必须等待加载更多视频。之后,您必须手动要求重新开始。为了解决这个问题,我按照以下步骤操作:

1) 检测:为了检测播放器何时停止,我使用 KVO 的值的 rate 属性:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"rate"] )
    {

        if (self.player.rate == 0 && CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) && self.videoPlaying)
        {
            [self continuePlaying];
        }
      }
    }

这个条件:CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime)是检测到达视频结尾和中间停的区别

2) 等待视频加载 - 如果您继续直接播放,您将没有足够的缓冲区继续播放而不会中断。要知道何时开始,您必须从 playerItem 观察值 playbackLikelytoKeepUp(这里我使用库来观察块,但我认为这很重要):

-(void)continuePlaying
 {

if (!self.playerItem.playbackLikelyToKeepUp)
{
    self.loadingView.hidden = NO;
    __weak typeof(self) wSelf = self;
    self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
        __strong typeof(self) sSelf = wSelf;
        if(sSelf)
        {
            if (sSelf.playerItem.playbackLikelyToKeepUp)
            {
                [sSelf.playerItem removeObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) token:self.playbackLikelyToKeepUpKVOToken];
                sSelf.playbackLikelyToKeepUpKVOToken = nil;
                [sSelf continuePlaying];
            }
                    }
    }];
}

就是这样!问题解决了

编辑:顺便说一下,使用的库是 libextobjc

【讨论】:

  • @weakify(self); self.playbackLikelyToKeepUpKVOToken 显示错误。
  • 我没有看到任何与简历相关的API调用,您的代码只是显示加载视图,如何通过代码再次播放视频?
  • self.videoPlaying 应该是什么?这是关于堆栈主题的唯一答案,它不适用于自然 obj c
  • 猜测playbackLikelyToKeepUpKVOToken 是一个布尔值,这个块继续给我带来问题。也许这表明我对这些观察者的误解,但我确信其他人一直在我的幼稚鞋子中,并且考虑到它在谷歌上的#1答案,它应该用代码更好地表达。 self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
  • 这段代码没有解决任何问题。它很难阅读,包含不容易设置的表达式和框架,并且不够流行以证明包含在答案中的合理性,而且代码没有做任何事情。 'continuePlaying' 不会向 avplayer 发出任何命令,它只是与观察者一起玩并递归调用自身(为什么??)。错误的答案。
【解决方案2】:

我认为使用AVPlayerItemPlaybackStalledNotification 来检测停滞是一种更好的方法。

【讨论】:

  • 我一直在使用此通知进行测试,但它似乎并非每次都会触发。我关闭互联网连接——等到缓冲区用完——它触发——重新打开互联网——等到播放器再次播放——等待 2 秒(见 A)——关闭互联网——让缓冲区用完 - 不会触发。答:通知似乎只有在我在测试周期之间等待约 10 秒时才会再次触发。
【解决方案3】:

我正在处理视频文件,因此我的代码比您需要的要多,但以下解决方案应该在播放器挂起时暂停播放器,然后每 0.5 秒检查一次,看看我们是否有足够的缓冲来跟上。如果是这样,它会重新启动播放器。如果播放器挂起超过 10 秒而没有重新启动,我们会停止播放器并向用户道歉。这意味着您需要合适的观察者。下面的代码对我来说效果很好。

在 .h 文件或其他地方定义/初始化的属性:

AVPlayer *player;  
int playerTryCount = -1; // this should get set to 0 when the AVPlayer starts playing
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

部分.m:

- (AVPlayer *)initializePlayerFromURL:(NSURL *)movieURL {
  // create AVPlayer
  AVPlayerItem *videoItem = [AVPlayerItem playerItemWithURL:movieURL];
  AVPlayer *videoPlayer = [AVPlayer playerWithPlayerItem:videoItem];

  // add Observers
  [videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
  [self startNotificationObservers]; // see method below
  // I observe a bunch of other stuff, but this is all you need for this to work

  return videoPlayer;
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  // check that all conditions for a stuck player have been met
  if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
      if (self.player.currentItem.playbackLikelyToKeepUp == NO &&
          CMTIME_COMPARE_INLINE(self.player.currentTime, >, kCMTimeZero) && 
          CMTIME_COMPARE_INLINE(self.player.currentTime, !=, self.player.currentItem.duration)) {

              // if so, post the playerHanging notification
              [self.notificationCenter postNotificationName:PlayerHangingNotification object:self.videoPlayer];
      }
  }
}

- (void)startNotificationObservers {
    [self.notificationCenter addObserver:self 
                                selector:@selector(playerContinue)
                                   name:PlayerContinueNotification
                                 object:nil];    

    [self.notificationCenter addObserver:self 
                                selector:@selector(playerHanging)
                                   name:PlayerHangingNotification
                                 object:nil];    
}

// playerHanging simply decides whether to wait 0.5 seconds or not
// if so, it pauses the player and sends a playerContinue notification
// if not, it puts us out of our misery
- (void)playerHanging {
    if (playerTryCount <= 10) {

      playerTryCount += 1;
      [self.player pause];
      // start an activity indicator / busy view
      [self.notificationCenter postNotificationName:PlayerContinueNotification object:self.player];

    } else { // this code shouldn't actually execute, but I include it as dummyproofing

      [self stopPlaying]; // a method where I clean up the AVPlayer,
                          // which is already paused

      // Here's where I'd put up an alertController or alertView
      // to say we're sorry but we just can't go on like this anymore
    }
}

// playerContinue does the actual waiting and restarting
- (void)playerContinue {
    if (CMTIME_COMPARE_INLINE(self.player.currentTime, ==, self.player.currentItem.duration)) { // we've reached the end

      [self stopPlaying];

    } else if (playerTryCount  > 10) // stop trying

      [self stopPlaying];
      // put up "sorry" alert

    } else if (playerTryCount == 0) {

      return; // protects against a race condition

    } else if (self.player.currentItem.playbackLikelyToKeepUp == YES) {

      // Here I stop/remove the activity indicator I put up in playerHanging
      playerTryCount = 0;
      [self.player play]; // continue from where we left off

    } else { // still hanging, not at end

        // create a 0.5-second delay to see if buffering catches up
        // then post another playerContinue notification to call this method again
        // in a manner that attempts to avoid any recursion or threading nightmares 
        playerTryCount += 1;
        double delayInSeconds = 0.5;
        dispatch_time_t executeTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
        dispatch_after(executeTime, dispatch_get_main_queue(), ^{

          // test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
          if (playerTryCount > 0) {
              if (playerTryCount <= 10) {
                [self.notificationCenter postNotificationName:PlayerContinueNotification object:self.videoPlayer];
              } else {
                [self stopPlaying];
                // put up "sorry" alert
              }
          }
        });
}

希望对你有帮助!

【讨论】:

  • 谢谢,您的解决方案有所帮助,我只需要进行一些更改。我没有观察关键路径“playbackLikelyToKeepUp”,而是使用了 NSNotification“AVPlayerItemPlaybackStalledNotification”,它在播放器停止时通知我的代码。我使用了 20 次重试的 playerTryCount 而不是 10 次,只是为了给互联网糟糕的用户更多的时间。其余的逻辑很棒!
【解决方案4】:

我遇到了类似的问题。 我有一些我想播放的本地文件,配置了 AVPlayer 并调用了 [player play],播放器在第 0 帧处停止并且不会再播放,直到我再次手动调用 play。 由于解释错误,我无法实施接受的答案,然后我只是尝试延迟播放并神奇地工作

[self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];

-(void)startVideo{
    [self.videoPlayer play];
}

对于网络视频,我也遇到了问题,我使用华莱士的答案解决了。

创建 AVPlayer 时添加观察者:

[self.videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
    if (self.videoPlayer.currentItem.playbackLikelyToKeepUp == NO &&
        CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, >, kCMTimeZero) &&
        CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, !=, self.videoPlayer.currentItem.duration)) {
        NSLog(@"hanged");
        [self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];
    }
}

}

记得在关闭视图之前移除观察者

[self.videoItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]

【讨论】:

    【解决方案5】:

    首先我观察到播放停止

    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerStalled),
        name: AVPlayerItemPlaybackStalledNotification, object: videoPlayer.currentItem)
    

    然后我强制播放继续

    func playerStalled(note: NSNotification) {
      let playerItem = note.object as! AVPlayerItem
      if let player = playerItem.valueForKey("player") as? AVPlayer{
        player.play()
      }
    }
    

    这可能不是最好的方法,但我一直在使用它,直到找到更好的方法:)

    【讨论】:

    • 根据Apple 系统可能会在用于注册观察者的线程之外的线程上发布此通知。,因此player.play() 可能被称为不同的线程。不知道这样行不行...
    【解决方案6】:

    已接受的答案为问题提供了可能的解决方案,但缺乏灵活性,也难以阅读。这是更灵活的解决方案。

    添加观察者:

    //_player is instance of AVPlayer
    [_player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
    [_player addObserver:self forKeyPath:@"rate" options:0 context:nil];
    

    处理程序:

    -(void)observeValueForKeyPath:(NSString*)keyPath
                         ofObject:(id)object
                           change:(NSDictionary*)change
                          context:(void*)context {
    
        if ([keyPath isEqualToString:@"status"]) {
            if (_player.status == AVPlayerStatusFailed) {
                //Possibly show error message or attempt replay from tart
                //Description from the docs:
                //  Indicates that the player can no longer play AVPlayerItem instances because of an error. The error is described by
                //  the value of the player's error property.
            }
        }else if ([keyPath isEqualToString:@"rate"]) {
            if (_player.rate == 0 && //if player rate dropped to 0
                    CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
                    CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
                    _isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
                [self handleStalled];
            }
        }
    }
    

    魔法:

    -(void)handleStalled {
        NSLog(@"Handle stalled. Available: %lf", [self availableDuration]);
    
        if (_player.currentItem.playbackLikelyToKeepUp || //
                [self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
            [_player play];
        } else {
            [self performSelector:@selector(handleStalled) withObject:nil afterDelay:0.5]; //try again
        }
    }
    

    “[self availableDuration]”是可选的,但您可以根据可用视频的数量手动启动播放。您可以更改代码检查是否缓冲了足够的视频的频率。如果你决定使用可选部分,这里是方法实现:

    - (NSTimeInterval) availableDuration
    {
        NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
        CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
        Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
        Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
        NSTimeInterval result = startSeconds + durationSeconds;
        return result;
    }
    

    别忘了清理。移除观察者:

    [_player.currentItem removeObserver:self forKeyPath:@"status"];
    [_player removeObserver:self forKeyPath:@"rate"];
    

    以及处理停滞视频的可能未决调用:

    [UIView cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleStalled) object:nil];
    

    【讨论】:

    • 根据文档 - "可能playbackLikelyToKeepUp 指示NO,而属性playbackBufferFull 指示YES。在这种情况下,播放缓冲区已达到容量但没有统计数据支持预测未来可能会跟上播放。是否继续播放媒体由您决定。“我认为检查playbackBufferFull也很重要
    【解决方案7】:

    在非常糟糕的网络中playbackLikelyToKeepUp 很可能是假的。

    使用kvo观察playbackBufferEmpty比较好,对是否存在可用于播放的缓冲区数据更敏感。如果值更改为true,则可以调用play方法继续播放。

    【讨论】:

    • 我认为你是对的,我认为 playbackLikelyToKeepUpplaybackBufferEmpty 上的 KVO 都是必需的。 Apple 说 - “playbackLikelyToKeepUp 可能指示 NO,而属性playbackBufferFull 指示 YES。在这种情况下,播放缓冲区已达到容量,但没有统计数据支持播放可能跟上的预测未来。是否继续播放媒体由您决定。"
    【解决方案8】:

    就我而言,

    • 我尝试使用 imagePickerController 录制视频并使用 AVPlayerController 播放录制的视频。但它开始播放视频并在 1 秒后停止。不知何故,它有时间保存视频,如果你立即播放它,它就不会播放。
      所以解决方案是,

    • 在 0.5 秒后调用播放视频(延迟)。如下所示

          -(void)imagePickerController:(UIImagePickerController *)picker 
                   didFinishPickingMediaWithInfo:(NSDictionary *)info {
                  [self performSelector:@selector(playVideo) withObject:self 
                  afterDelay:0.5];
            }
      
      
      -(void) playVideo {
      
         self.avPlayerViewController = [[AVPlayerViewController alloc] init];
            if(self.avPlayerViewController != nil)
          {
           AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:Vpath];
           AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
           self.avPlayerViewController.player = player;
           self.avPlayerViewController.showsPlaybackControls = NO;
           [self.avPlayerViewController setVideoGravity:AVLayerVideoGravityResizeAspectFill];
           [self.avPlayerViewController.view setFrame:[[UIScreen mainScreen] bounds]];
           self.avPlayerViewController.view.clipsToBounds = YES;
           self.avPlayerViewController.delegate = self;
           [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerDidFinishPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
           [self.viewVideoHolder addSubview:self.avPlayerViewController.view];
           [self.avPlayerViewController.player play];
      
       }
      

      }

       -(void) playerDidFinishPlaying
        {
          [avPlayer pause];
        }
      

    【讨论】:

      【解决方案9】:

      我也遇到过这个问题described here

      我在下面多次测试了这个答案,到目前为止每次都有效。

      这是我为 Swift 5 版本的@wallace's answer 提出的。

      1- 而不是观察 keyPath "playbackLikelyToKeepUp" 我使用 .AVPlayerItemPlaybackStalled Notification 并在里面通过 if !playerItem.isPlaybackLikelyToKeepUp {...} 检查缓冲区是否已满

      2- 我没有使用他的PlayerHangingNotification,而是使用了一个名为playerIsHanging()的函数

      3- 我没有使用他的PlayerContinueNotification,而是使用了一个名为checkPlayerTryCount()的函数

      4- 在checkPlayerTryCount() 内部,我所做的一切都与他的(void)playerContinue 函数相同,除非我遇到} else if playerTryCount == 0 { 什么都不会发生。为了避免这种情况,我在return 语句上方添加了两行代码

      5- 就像在@wallace 的 cmets 下建议的 @PranoyC 我将 playerTryCount 设置为最大值 20 而不是 10。我也将其设置为类属性let playerTryCountMaxLimit = 20

      您必须在评论建议的地方添加/删除活动指示器/微调器

      代码:

      NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
                                             name: NSNotification.Name.AVPlayerItemPlaybackStalled,
                                             object: playerItem)
      
      @objc func playerItemPlaybackStalled(_ notification: Notification) {
      // The system may post this notification on a thread other than the one used to registered the observer: https://developer.apple.com/documentation/foundation/nsnotification/name/1387661-avplayeritemplaybackstalled
      
          guard let playerItem = notification.object as? AVPlayerItem else { return }
          
          // playerItem.isPlaybackLikelyToKeepUp == false && if the player's current time is greater than zero && the player's current time is not equal to the player's duration
          if (!playerItem.isPlaybackLikelyToKeepUp) && (CMTimeCompare(playerItem.currentTime(), .zero) == 1) && (CMTimeCompare(playerItem.currentTime(), playerItem.duration) != 0) {
              
              DispatchQueue.main.async { [weak self] in
                  self?.playerIsHanging()
              }
          }
      }
      
      var playerTryCount = -1 // this should get set to 0 when the AVPlayer starts playing
      let playerTryCountMaxLimit = 20
      
      func playerIsHanging() {
          
          if playerTryCount <= playerTryCountMaxLimit {
              
              playerTryCount += 1
      
              // show spinner
      
              checkPlayerTryCount()
      
          } else {
              // show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
              print("1.-----> PROBLEM")
          }
      }
      
      func checkPlayerTryCount() {
          
          guard let player = player, let playerItem = player.currentItem else { return }
          
          // if the player's current time is equal to the player's duration
          if CMTimeCompare(playerItem.currentTime(), playerItem.duration) == 0 {
              
              // show spinner or better yet remove spinner and show a replayButton or auto rewind to the beginning ***BE SURE TO RESET playerTryCount = 0 ***
              
          } else if playerTryCount > playerTryCountMaxLimit {
              
              // show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
              print("2.-----> PROBLEM")
      
          } else if playerTryCount == 0 {
      
              // *** in his answer he has nothing but a return statement here but when it would hit this condition nothing would happen. I had to add these 2 lines of code for it to continue ***
              playerTryCount += 1
              retryCheckPlayerTryCountAgain()
              return // protects against a race condition
              
          } else if playerItem.isPlaybackLikelyToKeepUp {
              
              // remove spinner and reset playerTryCount to zero
              playerTryCount = 0
              player?.play()
      
          } else { // still hanging, not at end
              
              playerTryCount += 1
              
              /*
                create a 0.5-second delay using .asyncAfter to see if buffering catches up
                then call retryCheckPlayerTryCountAgain() in a manner that attempts to avoid any recursion or threading nightmares
              */
      
              DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                  DispatchQueue.main.async { [weak self] in
      
                      // test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
                      if self!.playerTryCount > 0 {
                          
                          if self!.playerTryCount <= self!.playerTryCountMaxLimit {
                              
                            self!.retryCheckPlayerTryCountAgain()
                              
                          } else {
                              
                            // show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
                            print("3.-----> PROBLEM")
                          }
                      }
                  }
              }
          }
      }
      
      func retryCheckPlayerTryCountAgain() {
          checkPlayerTryCount()
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-09-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多