web-dev-qa-db-ja.com

AVPlayerが再生を停止し、再び再開しない

私のアプリケーションでは、Webサーバーに保存されているオーディオファイルを再生する必要があります。 AVPlayerを使用しています。私には、すべての再生/一時停止コントロールと、デリゲートとオブザーバーがあり、完全に正常に動作します。小さなオーディオファイルを再生する場合、すべてがうまく機能します。

長いオーディオファイルを再生すると、再生も正常に開始されますが、数秒後にAVPlayerが再生を一時停止します(ほとんどの場合、バッファリングするため)。問題は、それ自体で再び再開しないことです。一時停止状態を維持し、手動で再生ボタンをもう一度押すと、再びスムーズに再生されます。

AVPlayerが自動的に再開しない理由と、ユーザーがもう一度再生ボタンを押さなくてもオーディオを再開する方法を知りたいのですが。ありがとう。

39
iAmd

はい、バッファが空であるため停止し、さらにビデオをロードするまで待機する必要があります。その後、手動で再起動を要求する必要があります。問題を解決するために、私は次の手順を実行しました。

1)検出:プレーヤーが停止したことを検出するには、値のrateプロパティでKVOを使用します。

_-(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です

16
Jpellat

私はビデオファイルで作業しているので、コードには必要以上のものがありますが、次の解決策はプレーヤーがハングしたときに一時停止し、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
              }
          }
        });
}

それが役に立てば幸い!

7
wallace

同様の問題がありました。再生したいローカルファイルがいくつかあり、AVPlayerを設定して[player play]と呼びました。プレーヤーはフレーム0で停止し、手動でもう一度playを呼び出すまで再生しません。説明が間違っていたため、受け入れられた答えを実装することは不可能でした。

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

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

Webビデオの場合も問題がありましたが、ウォレスの答えを使用して解決します。

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"]
6
gomezluisj

受け入れられた答えは問題の可能な解決策を提供しますが、柔軟性に欠けており、読みにくいです。これがより柔軟なソリューションです。

オブザーバーを追加します。

//_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];

AVPlayerItemPlaybackStalledNotificationを使用してストールを検出する方が良い方法だと思います。

5
xzysun

最初に再生の停止を観察します

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

これはおそらく最善の方法ではありませんが、より良いものが見つかるまで使用しています:)

2
budidino

非常に悪いネットワークplaybackLikelyToKeepUpでは、おそらくfalseになります。

kvoを使用してplaybackBufferEmptyの方が優れていることを確認します。再生に使用できる既存のバッファデータに敏感です。値がtrueに変わった場合、playメソッドを呼び出して再生を続行できます。

1
DumplingKid