Зная, когда объект AVPlayer готов к игре - PullRequest
65 голосов
/ 23 марта 2011

Я пытаюсь воспроизвести файл MP3, который передается в UIView из предыдущего UIView (хранится в переменной NSURL *fileURL).

Я инициализирую AVPlayer с помощью:

player = [AVPlayer playerWithURL:fileURL];

NSLog(@"Player created:%d",player.status);

NSLog печатает Player created:0,, что, как я понял, означает, что он еще не готов к игре.

Когда я нажимаю на игру UIButton, код, который я запускаю:

-(IBAction)playButtonClicked
{
    NSLog(@"Clicked Play. MP3:%@",[fileURL absoluteString]);

    if(([player status] == AVPlayerStatusReadyToPlay) && !isPlaying)
//  if(!isPlaying)
    {
        [player play];
        NSLog(@"Playing:%@ with %d",[fileURL absoluteString], player.status);
        isPlaying = YES;
    }
    else if(isPlaying)
    {

        [player pause];
        NSLog(@"Pausing:%@",[fileURL absoluteString]);
        isPlaying = NO;
    }
    else {
        NSLog(@"Error in player??");
    }

}

Когда я запускаю это, я всегда получаю Error in player?? в консоли. Однако если я заменим условие if, которое проверяет, готово ли AVPlayer к воспроизведению, простым if(!isPlaying) ..., то музыка воспроизводится ВТОРОЕ ВРЕМЯ, когда я нажимаю на воспроизведение UIButton.

Журнал консоли:

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 0**

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
Pausing:http://www.nimh.nih.gov/audio/neurogenesis.mp3

Clicked Play. MP3:http://www.nimh.nih.gov/audio/neurogenesis.mp3
2011-03-23 11:06:43.674 Podcasts[2050:207] Playing:http://www.nimh.nih.gov/audio/neurogenesis.mp3 **with 1**

Я вижу, что ВТОРОЕ ВРЕМЯ, player.status, кажется, содержит 1, что, я думаю, AVPlayerReadyToPlay.

Что я могу сделать, чтобы игра работала правильно при первом нажатии на игру UIButton? (т.е. как я могу убедиться, что AVPlayer не только создан, но и готов к игре?)

Ответы [ 9 ]

121 голосов
/ 23 марта 2011

Вы воспроизводите удаленный файл. AVPlayer может потребоваться некоторое время для буферизации достаточного количества данных и готовности к воспроизведению файла (см. Руководство по программированию AV Foundation )

Но вы, похоже, не ждете готовности игрока, прежде чем нажать кнопку воспроизведения. То, что я хотел бы сделать, это отключить эту кнопку и включить ее только тогда, когда игрок готов.

Используя KVO, можно получать уведомления об изменениях статуса игрока:

playButton.enabled = NO;
player = [AVPlayer playerWithURL:fileURL];
[player addObserver:self forKeyPath:@"status" options:0 context:nil];   

Этот метод будет вызываться при изменении статуса:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                        change:(NSDictionary *)change context:(void *)context {
    if (object == player && [keyPath isEqualToString:@"status"]) {
        if (player.status == AVPlayerStatusReadyToPlay) {
            playButton.enabled = YES;
        } else if (player.status == AVPlayerStatusFailed) {
            // something went wrong. player.error should contain some information
        }
    }
}
28 голосов
/ 17 марта 2012

У меня было много проблем при попытке выяснить статус AVPlayer.Свойство status не всегда казалось очень полезным, и это приводило к бесконечному разочарованию, когда я пытался обрабатывать прерывания аудио сеанса.Иногда AVPlayer говорил мне, что готов играть (с AVPlayerStatusReadyToPlay), хотя на самом деле это не так.Я использовал метод Кило Джилоука, но он работал не во всех случаях.

В дополнение, когда свойство status бесполезно, я запросил объем потока, который AVPlayer загрузил, просмотрев свойство loadedTimeRanges currentItem *1009* (который является AVPlayerItem).

Все это немного сбивает с толку, но вот как это выглядит:

NSValue *val = [[[audioPlayer currentItem] loadedTimeRanges] objectAtIndex:0];
CMTimeRange timeRange;
[val getValue:&timeRange];
CMTime duration = timeRange.duration;
float timeLoaded = (float) duration.value / (float) duration.timescale; 

if (0 == timeLoaded) {
    // AVPlayer not actually ready to play
} else {
    // AVPlayer is ready to play
}
13 голосов
/ 13 декабря 2017

Swift Solution

var observer: NSKeyValueObservation?

func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played
    let asset = AVAsset(url: url)

    let assetKeys = [
        "playable",
        "hasProtectedContent"
    ]
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    let playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: assetKeys)

    // Register as an observer of the player item's status property
    self.observer = playerItem.observe(\.status, options:  [.new, .old], changeHandler: { (playerItem, change) in
        if playerItem.status == .readyToPlay {
            //Do your work here
        }
    })

    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}

Также вы можете аннулировать наблюдателя таким образом

self.observer.invalidate()

Важно: Вы должны оставить переменную наблюдателя сохраненной, иначе она освободится, и changeHandler больше не будет вызываться Поэтому не определяйте наблюдателя как переменную функции, а определяйте его как переменную экземпляра, как в данном примере.

Этот синтаксис наблюдателя значения ключа является новым для Swift 4.

Подробнее см. Здесь https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/Contents.swift

10 голосов
/ 10 марта 2016

После долгих исследований и многих попыток я заметил, что обычно наблюдатель status не лучше знать, когда объект AVPlayer готов к игре , потому что объект может бытьготов к игре, но это не значит, что он будет немедленно воспроизведен.

Лучше знать, что это с loadedTimeRanges.

Для регистрационного наблюдателя

[playerClip addObserver:self forKeyPath:@"currentItem.loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

Прослушивание наблюдателя

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (object == playerClip && [keyPath isEqualToString:@"currentItem.loadedTimeRanges"]) {
        NSArray *timeRanges = (NSArray*)[change objectForKey:NSKeyValueChangeNewKey];
        if (timeRanges && [timeRanges count]) {
            CMTimeRange timerange=[[timeRanges objectAtIndex:0]CMTimeRangeValue];
            float currentBufferDuration = CMTimeGetSeconds(CMTimeAdd(timerange.start, timerange.duration));
            CMTime duration = playerClip.currentItem.asset.duration;
            float seconds = CMTimeGetSeconds(duration);

            //I think that 2 seconds is enough to know if you're ready or not
            if (currentBufferDuration > 2 || currentBufferDuration == seconds) {
                // Ready to play. Your logic here
            }
        } else {
            [[[UIAlertView alloc] initWithTitle:@"Alert!" message:@"Error trying to play the clip. Please try again" delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil] show];
        }
    }
}

Для удаления наблюдателя (dealloc, viewWillDissapear или перед регистром-наблюдателем) это хорошие места для вызова

- (void)removeObserverForTimesRanges
{
    @try {
        [playerClip removeObserver:self forKeyPath:@"currentItem.loadedTimeRanges"];
    } @catch(id anException){
        NSLog(@"excepcion remove observer == %@. Remove previously or never added observer.",anException);
        //do nothing, obviously it wasn't attached because an exception was thrown
    }
}
5 голосов
/ 16 июня 2017
private var playbackLikelyToKeepUpContext = 0

Для регистрационного наблюдателя

avPlayer.addObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp",
        options: .new, context: &playbackLikelyToKeepUpContext)

Слушай наблюдателя

 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if context == &playbackLikelyToKeepUpContext {
        if avPlayer.currentItem!.isPlaybackLikelyToKeepUp {
           // loadingIndicatorView.stopAnimating() or something else
        } else {
           // loadingIndicatorView.startAnimating() or something else
        }
    }
}

Для удаления наблюдателя

deinit {
    avPlayer.removeObserver(self, forKeyPath: "currentItem.playbackLikelyToKeepUp")
}

Ключевым моментом в коде является свойство экземпляра isPlaybackLikelyToKeepUp.

4 голосов
/ 27 апреля 2017

На основании ответа Тима Камбера , вот функция Swift, которую я использую:

private func isPlayerReady(_ player:AVPlayer?) -> Bool {

    guard let player = player else { return false }

    let ready = player.status == .readyToPlay

    let timeRange = player.currentItem?.loadedTimeRanges.first as? CMTimeRange
    guard let duration = timeRange?.duration else { return false } // Fail when loadedTimeRanges is empty
    let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
    let loaded = timeLoaded > 0

    return ready && loaded
}

Или, как расширение

extension AVPlayer {
    var ready:Bool {
        let timeRange = currentItem?.loadedTimeRanges.first as? CMTimeRange
        guard let duration = timeRange?.duration else { return false }
        let timeLoaded = Int(duration.value) / Int(duration.timescale) // value/timescale = seconds
        let loaded = timeLoaded > 0

        return status == .readyToPlay && loaded
    }
}
4 голосов
/ 26 августа 2015

У меня были проблемы с невозможностью получения обратных вызовов.

Оказывается, это зависит от того, как вы создаете поток.В моем случае я использовал playerItem для инициализации, и поэтому мне пришлось добавить наблюдателя вместо элемента.

Например:

- (void) setup
{
    ...
    self.playerItem = [AVPlayerItem playerItemWithAsset:asset];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    ... 

     // add callback
     [self.player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
}

// the callback method
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                    change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"[VideoView] player status: %i", self.player.status);

    if (object == self.player.currentItem && [keyPath isEqualToString:@"status"])
    {
        if (self.player.currentItem.status == AVPlayerStatusReadyToPlay)
        {
           //do stuff
        }
    }
}

// cleanup or it will crash
-(void)dealloc
{
    [self.player.currentItem removeObserver:self forKeyPath:@"status"];
}
1 голос
/ 23 марта 2011

Проверка статуса текущего элемента игрока:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
0 голосов
/ 03 июля 2019

Swift 5:

var player:AVPlayer!

override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, 
               selector: #selector(playerItemDidReadyToPlay(notification:)),
               name: .AVPlayerItemNewAccessLogEntry, 
               object: player?.currentItem)
}

@objc func playerItemDidReadyToPlay(notification: Notification) {
        if let _ = notification.object as? AVPlayerItem {
            // player is ready to play now!!
        }
}
...