Wissen, wann das AVPlayer-Objekt spielbereit ist

79

Ich versuche, eine MP3Datei abzuspielen , die an UIVieweine frühere UIView(in einer NSURL *fileURLVariablen gespeichert ) übergeben wird.

Ich initialisiere ein AVPlayermit:

player = [AVPlayer playerWithURL:fileURL];

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

Die NSLogDrucke, Player created:0,die ich mir vorgestellt habe, bedeuten, dass es noch nicht spielbereit ist.

Wenn ich auf die Wiedergabe UIButtonklicke, lautet der von mir ausgeführte Code:

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

}

Wenn ich das starte, steige ich immer Error in player??in die Konsole. Wenn ich jedoch die ifBedingung, die prüft, ob AVPlayersie spielbereit ist, durch eine einfache if(!isPlaying)... ersetze , wird die Musik das ZWEITE MAL abgespielt , wenn ich auf die Wiedergabe klicke UIButton.

Das Konsolenprotokoll lautet:

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

Ich sehe, dass das ZWEITE MAL player.status1 zu halten scheint, was ich vermute AVPlayerReadyToPlay.

Was kann ich tun, damit das Spielen beim ersten Klicken richtig funktioniert UIButton? (dh wie kann ich sicherstellen, dass das AVPlayernicht nur erstellt, sondern auch spielbereit ist?)

mvishnu
quelle

Antworten:

127

Sie spielen eine entfernte Datei ab. Es kann einige Zeit dauern, AVPlayerbis genügend Daten gepuffert sind und die Datei abgespielt werden kann (siehe AV Foundation-Programmierhandbuch ).

Sie scheinen jedoch nicht darauf zu warten, dass der Player bereit ist, bevor Sie auf die Wiedergabetaste tippen. Ich würde diese Schaltfläche deaktivieren und nur aktivieren, wenn der Player bereit ist.

Mit KVO können Sie über Änderungen des Spielerstatus benachrichtigt werden:

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

Diese Methode wird aufgerufen, wenn sich der Status ändert:

- (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
quelle
Vielen Dank!! Das hat wie ein Zauber gewirkt. (hätte allerdings
raten
Es gibt einige URLs, die einfach nicht abgespielt werden. Sie sind vorhanden, funktionieren jedoch nicht (zum Beispiel spielt iTunes sie nicht ab). Wie gehen Sie mit diesem Verhalten um? In AVPlayer gibt es keine Zeitüberschreitung.
Fabrizio
10
Nach meiner Erfahrung player.currentItem.statusist genau, wenn player.statusnicht. Ich bin mir nicht sicher, was die Unterschiede sind.
Bendytree
1
@iOSAppDev Verwenden Sie unter IOS7 AVPlayerItem addObserver
Peter Zhao
4
Wow, dieser AVPlayer ist so schlecht designt, dass ich weinen muss. Warum nicht einen onLoad-Handlerblock hinzufügen? Komm schon Apple, vereinfache deine Sachen!
Ente
30

Ich hatte große Probleme, den Status eines herauszufinden AVPlayer. Das statusAnwesen schien nicht immer furchtbar hilfreich zu sein, und dies führte zu endloser Frustration, als ich versuchte, mit Unterbrechungen der Audiositzung umzugehen. Manchmal AVPlayersagte mir das, es sei bereit zu spielen, AVPlayerStatusReadyToPlaywenn es nicht wirklich zu sein schien. Ich habe die KVO-Methode von Jilouc verwendet, aber sie hat nicht in allen Fällen funktioniert.

Als Ergänzung dazu habe ich, wenn die Statuseigenschaft nicht nützlich war, die Menge des Streams abgefragt, den der AVPlayer geladen hatte, indem ich mir die loadedTimeRangesEigenschaft der AVPlayer's angesehen habe currentItem(die eine ist AVPlayerItem).

Es ist alles ein wenig verwirrend, aber so sieht es aus:

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
quelle
2
Der NSValue-Typ wird mit AV Foundation erweitert. Mit einigen dieser Helfer können Sie von NSValue zu CMTimeXxx-Werten hin und her konvertieren. Wie CMTimeRangeValue .
Superjos
Ähnliche Geschichte, um Sekunden (ich denke, das timeLoadedist es , was ist) aus CMTime herauszuholen: CMTimeGetSeconds
Superjos
2
Leider sollte dies eine akzeptierte Antwort sein. AVPlayerscheint status == AVPlayerStatusReadyToPlayzu früh zu setzen, wenn es nicht wirklich spielbereit ist. Damit dies funktioniert, können Sie beispielsweise den obigen Code in einen NSTimerAufruf einschließen.
Maxkonovalov
Könnte es sein, dass mehr als (wlog) 2 Sekunden geladener Zeitbereich vorhanden sind, der Status des Players oder des PlayerItems jedoch nicht ReadyToPlay ist? IOW, sollte es auch bestätigt werden?
Danielhadar
28

Schnelle Lösung

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

Auf diese Weise können Sie auch den Beobachter ungültig machen

self.observer.invalidate()

Wichtig: Sie müssen die Beobachtervariable beibehalten, da sonst die Zuordnung aufgehoben wird und der changeHandler nicht mehr aufgerufen wird. Definieren Sie den Beobachter also nicht als Funktionsvariable, sondern als Instanzvariable wie im angegebenen Beispiel.

Diese Schlüsselwert-Beobachter-Syntax ist neu in Swift 4.

Weitere Informationen finden Sie hier 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
quelle
Danke, diese Methode ist sehr einfach zum Entfernen von KVO.
ZAFAR007
11

Nachdem ich viel recherchiert und viele Möglichkeiten ausprobiert habe, habe ich festgestellt, dass der statusBeobachterAVPlayer normalerweise nicht besser weiß, wann das Objekt spielbereit ist , da das Objekt spielbereit sein kann, aber dies bedeutet nicht, dass es sofort abgespielt wird.

Die bessere Idee, dies zu wissen, ist mit loadedTimeRanges.

Für Registerbeobachter

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

Hören Sie dem Beobachter zu

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

Zum Entfernen von Beobachtern (Dealloc, ViewWillDissapear oder vor dem Registrieren von Beobachtern) ist es ein guter Ort zum Anrufen

- (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
quelle
Danke, das hat auch bei mir funktioniert. Ich habe jedoch nicht die Auswertung "currentBufferDuration == Sekunden" verwendet. Könnten Sie mir bitte sagen, wofür es verwendet wird?
Andrei
Für Fälle, in denencurrentBufferDuration < 2
jose920405
Könnte es sein, dass mehr als (wlog) 2 Sekunden geladener Zeitbereich vorhanden sind, der Status des Players oder des PlayerItems jedoch nicht ReadyToPlay ist? IOW, sollte es auch bestätigt werden?
Danielhadar
11
private var playbackLikelyToKeepUpContext = 0

Für Registerbeobachter

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

Hören Sie dem Beobachter zu

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

Zum Entfernen des Beobachters

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

Der Schlüsselpunkt im Code ist die Instanzeigenschaft isPlaybackLikelyToKeepUp.

Harman
quelle
3
Gute Antwort. Ich würde KVO mitforKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Miroslav Hrivik
Für 2019 funktioniert das perfekt - Kopieren und Einfügen :) Ich habe den Mod von @MiroslavHrivik verwendet, danke!
Fattie
7

Basierend auf der Antwort von Tim Camber ist hier die Swift-Funktion, die ich benutze:

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
}

Oder als Erweiterung

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
quelle
Mit der Erweiterung ist es wohl nicht möglich, die Ready-Eigenschaft von KVO zu beobachten. Irgendwo herum?
Jonny
Ich höre mir die Benachrichtigungen AVPlayerItemNewAccessLogEntryund AVPlayerItemDidPlayToEndTimein meinem Projekt an. Afaik es funktioniert.
Axel Guilmin
OK, am Ende habe ich zugehört loadedTimeRanges.
Jonny
5

Ich hatte Probleme, keine Rückrufe zu erhalten.

Es stellt sich heraus, dass es davon abhängt, wie Sie den Stream erstellen. In meinem Fall habe ich ein playerItem zum Initialisieren verwendet und musste stattdessen den Beobachter zum Element hinzufügen.

Zum Beispiel:

- (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
quelle
Das wenn sollte nicht mit sein AVPlayerItemStatusReadyToPlay?
Jose920405
@ jose920405 Ich kann bestätigen, dass die obige Lösung funktioniert, aber es ist eine gute Frage. Ich weiß es wirklich nicht. Lassen Sie mich wissen, wenn Sie es testen.
dac2009
3

Überprüfen Sie den Status des aktuellen Elements des Players:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Kirby Todd
quelle
2
player.currentItem.status gibt AVPlayerItemStatusUnkown zurück. Ich weiß nicht, was ich als nächstes tun soll. :(
Mvishnu
Anfangs ist dieser Wert AVPlayerItemStatusUnkown. Erst nach einiger Zeit wird es wissen können, ob es ist AVPlayerItemStatusReadyToPlayoderAVPlayerItemStatusFailed
Gustavo Barbosa
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!!
        }
}
Alessandro Ornano
quelle
1

@ JoshBernfelds Antwort hat bei mir nicht funktioniert. Nicht sicher warum. Er beobachtete playerItem.observe(\.status. Ich musste beobachten player?.observe(\.currentItem?.status. Scheint, als wären sie dasselbe, das playerItem statusEigentum.

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

Wenn Sie mit dem Player-Set fertig sind playerStatusObserver = nil

Lance Samaria
quelle