Знание, когда объект AVPlayer готов к воспроизведению

79

Я пытаюсь воспроизвести 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он не просто создан, но и готов к игре?)

Мвишну
источник

Ответы:

127

Вы проигрываете удаленный файл. 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
        }
    }
}
Жилук
источник
Спасибо!! Это сработало как шарм. (хотя должен был догадаться, когда увидел, что он без проблем проигрывает офлайн-файлы)
mvishnu
Есть некоторые URL-адреса, которые просто не воспроизводятся, они существуют, но они не работают (например, iTunes тоже их не воспроизводит). Как вы справляетесь с таким поведением? В AVPlayer нет тайм-аута.
Fabrizio
10
По моему опыту player.currentItem.statusверно, когда player.statusнет. Не уверен, в чем разница.
bendytree
1
@iOSAppDev На IOS7 используйте AVPlayerItem addObserver
Питер Чжао
4
вау, этот AVPlayer настолько плохо спроектирован, что я плачу. Почему бы не добавить блок обработчика onLoad? Давай, Apple, упрости свои дела!
Дак
30

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

В дополнении, когда свойство статуса не является полезным, я оспаривал сумму потока , что AVPlayer был загружен, глядя на loadedTimeRangesсвойстве AVPlayer«с currentItem(который является 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
}
Тим Арнольд
источник
2
В AV Foundation есть дополнения к типу NSValue. Некоторые из этих помощников позволяют преобразовывать значения NSValue в CMTimeXxx и обратно. Как CMTimeRangeValue .
superjos
Похожая история для получения секунд (я думаю, что это то, что timeLoadedесть) из CMTime: CMTimeGetSeconds
superjos
2
К сожалению, это должен быть принятый ответ. AVPlayerкажется, устанавливается status == AVPlayerStatusReadyToPlayслишком рано, когда он еще не готов к игре. Чтобы это работало, вы можете, например, обернуть приведенный выше код в NSTimerвызов.
maxkonovalov
Может ли быть случай, когда загруженный временной диапазон превышает (wlog) 2 секунды, но статус игрока или playerItem не является ReadyToPlay? IOW, это тоже нужно подтвердить?
danielhadar
29

Быстрое решение

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

Джош Бернфельд
источник
Спасибо, этот способ очень простой для удаления KVO.
ZAFAR007 05
11

После большого исследования и опробования многих способов я заметил, что обычно 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
    }
}
jose920405
источник
спасибо, это сработало и для меня. Однако я не использовал оценку «currentBufferDuration == секунды». Подскажите, пожалуйста, для чего он нужен?
Андрей
Для случаев, когдаcurrentBufferDuration < 2
jose920405
Может ли быть случай, когда загруженный временной диапазон превышает (wlog) 2 секунды, но статус игрока или playerItem не является ReadyToPlay? IOW, это тоже нужно подтвердить?
danielhadar
11
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.

Харман
источник
3
Хороший ответ. Я бы улучшил КВО с помощьюforKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Мирослав Хривик 01
В 2019 году это работает отлично - скопируйте и вставьте :) Я использовал мод @MiroslavHrivik, спасибо!
Fattie
7

Основываясь на ответе Тима Камбера , я использую функцию 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
    }
}
Аксель Гильмин
источник
С расширением, я думаю, KVO не может наблюдать готовое свойство. В любом случае?
Джонни
Слушаю уведомления AVPlayerItemNewAccessLogEntryи AVPlayerItemDidPlayToEndTimeв своем проекте. Афаик работает.
Axel Guilmin
ОК, в итоге я послушал loadedTimeRanges.
Джонни
5

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

Оказывается, это зависит от того, как вы создаете поток. В моем случае я использовал 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"];
}
dac2009
источник
Если не должно быть с AVPlayerItemStatusReadyToPlay?
jose920405
@ jose920405 Я могу подтвердить, что приведенное выше решение работает, но это хороший вопрос. Я правда не знаю. Сообщите мне, если вы это протестируете
dac2009,
3

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

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Кирби Тодд
источник
2
player.currentItem.status возвращает AVPlayerItemStatusUnkown. Я не знаю, что делать дальше. :(
mvishnu
Изначально это значение AVPlayerItemStatusUnkown. Только через какое - то время, он будет иметь возможность знать , если он AVPlayerItemStatusReadyToPlayилиAVPlayerItemStatusFailed
Густаво Барбоса
2

Swift 4:

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!!
        }
}
Алессандро Орнано
источник
1

Ответ @JoshBernfeld не сработал для меня. Не знаю почему. Он заметил playerItem.observe(\.status. Я должен был наблюдать player?.observe(\.currentItem?.status. Похоже, это одно и то же, playerItem statusсобственность.

var playerStatusObserver: NSKeyValueObservation?

player?.automaticallyWaitsToMinimizeStalling = false // starts faster

playerStatusObserver = player?.observe(\.currentItem?.status, options: [.new, .old]) { (player, change) in
        
    switch (player.status) {
    case .readyToPlay:
            // here is where it's ready to play so play player
            DispatchQueue.main.async { [weak self] in
                self?.player?.play()
            }
    case .failed, .unknown:
            print("Media Failed to Play")
    @unknown default:
         break
    }
}

когда вы закончите использовать набор плеера playerStatusObserver = nil

Копье Самария
источник