Wiedza, kiedy obiekt AVPlayer jest gotowy do gry

79

Próbuję odtworzyć MP3plik, który jest przekazywany UIViewz poprzedniego UIView(przechowywany w NSURL *fileURLzmiennej).

Inicjalizuję za AVPlayerpomocą:

player = [AVPlayer playerWithURL:fileURL];

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

Te NSLogodciski Player created:0,który Pomyślałem oznacza, że nie jest jeszcze gotowy do gry.

Kiedy klikam przycisk odtwarzania UIButton, uruchamiany kod to:

-(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??");
    }

}

Kiedy to uruchamiam, zawsze wchodzę Error in player??do konsoli. Jeśli jednak zmienię ifwarunek, który sprawdza, czy AVPlayerjest gotowy do odtwarzania, prostym if(!isPlaying)..., wtedy muzyka jest odtwarzana DRUGI RAZ, kiedy klikam grę UIButton.

Dziennik konsoli to:

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**

Widzę, że DRUGI CZAS player.statuswydaje się mieć 1, co, jak sądzę, jest AVPlayerReadyToPlay.

Co mogę zrobić, aby gra działała prawidłowo po pierwszym kliknięciu UIButton? (tj. jak mogę się upewnić, że AVPlayerplik nie jest tylko utworzony, ale także gotowy do gry?)

mvishnu
źródło

Odpowiedzi:

127

Odtwarzasz plik zdalny. AVPlayerZbuforowanie wystarczającej ilości danych i przygotowanie się do odtworzenia pliku może zająć trochę czasu (patrz Przewodnik programowania AV Foundation )

Ale wydaje się, że nie czekasz, aż gracz będzie gotowy, zanim naciśniesz przycisk odtwarzania. Chciałbym wyłączyć ten przycisk i włączyć go dopiero wtedy, gdy odtwarzacz jest gotowy.

Za pomocą KVO można otrzymywać powiadomienia o zmianach statusu gracza:

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

Ta metoda zostanie wywołana, gdy stan się zmieni:

- (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
        }
    }
}
Jilouc
źródło
Dziękuję Ci!! To zadziałało jak urok. (chociaż powinien się domyślić, kiedy zobaczył, że grał plików offline bez problemu)
mvishnu
Istnieje kilka adresów URL, które po prostu nie są odtwarzane, istnieją, ale nie działają (na przykład iTunes też ich nie odtworzy). Jak radzisz sobie z tym zachowaniem? W programie AVPlayer nie ma limitu czasu.
Fabrizio
10
Z mojego doświadczenia player.currentItem.statuswynika, że player.statusnie jest. Nie wiem, jakie są różnice.
bendytree
1
@iOSAppDev Na IOS7 użyj AVPlayerItem addObserver
Peter Zhao
4
wow, ten odtwarzacz AVPlayer jest tak źle zaprojektowany, że płaczę. Dlaczego nie dodać bloku obsługi onLoad? Dalej Apple, uprość swoje rzeczy!
Duck
30

Miałem wiele problemów z ustaleniem statusu pliku AVPlayer. statusNieruchomość nie zawsze wydają się być strasznie pomocny, a to doprowadziło do niekończącej się frustracji, kiedy próbuje obsłużyć dźwiękowych przerw sesji. Czasami AVPlayermówili mi, że jest gotowy do gry (z AVPlayerStatusReadyToPlay), podczas gdy tak naprawdę nie wyglądał. Użyłem metody KVO firmy Jilouc, ale nie działała we wszystkich przypadkach.

Aby uzupełnić, gdy właściwość status nie była użyteczna, zapytałem o ilość strumienia załadowanego przez AVPlayer, patrząc na loadedTimeRangeswłaściwość AVPlayer's currentItem(która jest AVPlayerItem).

To trochę zagmatwane, ale tak to wygląda:

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
}
Tim Arnold
źródło
2
Istnieją dodatki do typu NSValue dostarczane z AV Foundation. Niektóre z tych pomocników pozwalają na konwersję z wartości NSValue do CMTimeXxx. Podobnie jak CMTimeRangeValue .
superjos
Podobna historia o uzyskiwaniu sekund (chyba tak timeLoadedjest) z CMTime: CMTimeGetSeconds
superjos
2
Niestety, powinna to być akceptowana odpowiedź. AVPlayerwydaje się ustawiać status == AVPlayerStatusReadyToPlayzbyt wcześnie, gdy naprawdę nie jest gotowy do gry. Aby to zadziałało, możesz NSTimerna przykład opakować powyższy kod w wywołanie.
maxkonovalov
Czy może to być przypadek, w którym istnieją ponad (wlog) 2 sekundy załadowanego zakresu czasu, ale status gracza lub playerItem nie jest ReadyToPlay? IOW, czy to też powinno być potwierdzone?
danielhadar
29

Szybkie rozwiązanie

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

W ten sposób możesz również unieważnić obserwatora

self.observer.invalidate()

Ważne: musisz zachować zmienną obserwatora, w przeciwnym razie zostanie ona zwolniona, a changeHandler nie będzie już wywoływany. Dlatego nie definiuj obserwatora jako zmiennej funkcji, ale definiuj ją jako zmienną instancji, tak jak w podanym przykładzie.

Ta składnia obserwatora wartości klucza jest nowa w języku Swift 4.

Więcej informacji można znaleźć tutaj: https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/ Contents.swift

Josh Bernfeld
źródło
Dzięki, ta metoda jest bardzo prosta do usunięcia KVO.
ZAFAR007
11

Po wielu badaniach i wypróbowaniu wielu sposobów zauważyłem, że zwykle statusobserwator nie jest lepszy, aby wiedzieć, kiedy AVPlayerobiekt jest gotowy do gry , ponieważ obiekt może być gotowy do gry, ale nie oznacza to, że będzie odtwarzany natychmiast.

Lepszy pomysł, aby wiedzieć, że to jest z loadedTimeRanges.

Rejestracja obserwatora

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

Posłuchaj obserwatora

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

Do usuwania obserwatora (dealloc, viewWillDissapear lub before register observer) jest to dobre miejsce na wywołanie

- (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
źródło
dzięki, to też zadziałało. Jednak nie użyłem oceny „currentBufferDuration == sekundy”. Czy mógłbyś mi powiedzieć, do czego to służy?
Andrei
W przypadkach, gdycurrentBufferDuration < 2
jose920405
Czy może to być przypadek, w którym istnieją ponad (wlog) 2 sekundy załadowanego zakresu czasu, ale status gracza lub playerItem nie jest ReadyToPlay? IOW, czy to też powinno być potwierdzone?
danielhadar
11
private var playbackLikelyToKeepUpContext = 0

Dla obserwatora rejestru

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

Posłuchaj obserwatora

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

Aby usunąć obserwatora

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

Kluczowym punktem w kodzie jest właściwość instancji isPlaybackLikelyToKeepUp.

Harman
źródło
3
Dobra odpowiedź. forKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Poprawiłbym
W 2019 działa to idealnie - kopiuj i wklej :) Użyłem moda @MiroslavHrivik, dzięki!
Fattie
7

Na podstawie odpowiedzi Tima Cambera , oto funkcja Swift, której używam:

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
}

Lub jako rozszerzenie

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
    }
}
Axel Guilmin
źródło
Przy rozbudowie chyba nie jest możliwe obserwowanie gotowej nieruchomości przez KVO. Jakiś sposób?
Jonny
Słucham powiadomień AVPlayerItemNewAccessLogEntryiw AVPlayerItemDidPlayToEndTimemoim projekcie. Afaik to działa.
Axel Guilmin
OK, w końcu posłuchałem loadedTimeRanges.
Jonny
5

Miałem problemy z nieotrzymaniem żadnych oddzwonień.

Okazuje się, że zależy to od tego, jak utworzysz strumień. W moim przypadku użyłem playerItem do zainicjowania, więc zamiast tego musiałem dodać obserwatora do elementu.

Na przykład:

- (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
źródło
Jeśli nie powinno być z AVPlayerItemStatusReadyToPlay?
jose920405
@ jose920405 Potwierdzam, że powyższe rozwiązanie działa, ale to dobre pytanie. Naprawdę nie wiem. Daj mi znać, jeśli to przetestujesz.
dac2009
3

Sprawdź status currentItem gracza:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Kirby Todd
źródło
2
player.currentItem.status zwraca AVPlayerItemStatusUnkown. Nie wiem, co robić dalej. :(
mvishnu
Początkowo ta wartość to AVPlayerItemStatusUnkown. Dopiero za jakiś czas będzie mógł wiedzieć, czy tak jest, AVPlayerItemStatusReadyToPlayczyAVPlayerItemStatusFailed
Gustavo Barbosa
3

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!!
        }
}
Alessandro Ornano
źródło
1

Odpowiedź @ JoshBernfeld nie działa dla mnie. Nie pewny dlaczego. Obserwował playerItem.observe(\.status. Musiałem obserwować player?.observe(\.currentItem?.status. Wygląda na to, że to to samo, playerItem statuswłasność.

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

po zakończeniu korzystania z zestawu odtwarzacza playerStatusObserver = nil

Lance Samaria
źródło