web-dev-qa-db-fra.com

Savoir quand l'objet AVPlayer est prêt à jouer

J'essaie de lire un fichier MP3 Qui est passé à un UIView d'une précédente UIView (stockée dans une variable NSURL *fileURL).

J'initialise un AVPlayer avec:

player = [AVPlayer playerWithURL:fileURL];

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

Le NSLog imprime Player created:0, Qui, selon moi, signifie qu'il n'est pas encore prêt à jouer.

Lorsque je clique sur la lecture UIButton, le code que je lance est:

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

}

Quand je lance ceci, j'obtiens toujours Error in player?? Dans la console. Si je remplace cependant la condition if qui vérifie si AVPlayer est prête à jouer, avec une simple if(!isPlaying)..., alors la musique joue la DEUXIÈME FOIS sur laquelle je clique le jeu UIButton.

Le journal de la console est:

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

Je vois que la DEUXIÈME FOIS, le player.status Semble contenir 1, ce qui, je suppose, est AVPlayerReadyToPlay.

Que puis-je faire pour que le jeu fonctionne correctement la première fois que je clique sur le jeu UIButton? (c.-à-d., comment puis-je m'assurer que le AVPlayer n'est pas seulement créé, mais aussi prêt à jouer?)

69
mvishnu

Vous lisez un fichier distant. Cela peut prendre un certain temps au AVPlayer pour mettre en mémoire tampon suffisamment de données et être prêt à lire le fichier (voir AV Foundation Programming Guide )

Mais vous ne semblez pas attendre que le joueur soit prêt avant d'appuyer sur le bouton de lecture. Ce que je voudrais, c'est désactiver ce bouton et l'activer uniquement lorsque le lecteur est prêt.

En utilisant KVO, il est possible d'être averti des changements de statut du joueur:

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

Cette méthode sera appelée lorsque le statut change:

- (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
        }
    }
}
122
Jilouc

J'ai eu beaucoup de mal à essayer de comprendre l'état d'un AVPlayer. La propriété status ne semblait pas toujours être extrêmement utile, et cela a conduit à une frustration sans fin lorsque j'essayais de gérer les interruptions de session audio. Parfois, le AVPlayer m'a dit qu'il était prêt à jouer (avec AVPlayerStatusReadyToPlay) alors qu'il ne semblait pas l'être. J'ai utilisé la méthode KVO de Jilouc, mais cela n'a pas fonctionné dans tous les cas.

Pour compléter, lorsque la propriété status n'était pas utile, j'ai interrogé la quantité de flux que AVPlayer avait chargée en examinant la propriété loadedTimeRanges de AVPlayer's currentItem (qui est un AVPlayerItem).

C'est un peu déroutant, mais voici à quoi ça ressemble:

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
}
29
Tim Camber

Solution rapide

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

Vous pouvez également invalider l'observateur de cette façon

self.observer.invalidate()

Important: vous devez conserver la variable d'observateur, sinon elle sera désallouée et le changeHandler ne sera plus appelé. Ne définissez donc pas l'observateur comme une variable de fonction, mais définissez-le comme une variable d'instance comme dans l'exemple donné.

Cette syntaxe d'observateur de valeur clé est nouvelle pour Swift 4.

Pour plus d'informations, voir ici https://github.com/ole/whats-new-in-Swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key% 20paths.xcplaygroundpage/Contents.Swift

15
Josh Bernfeld

Après avoir fait beaucoup de recherches et essayé de nombreuses façons, j'ai remarqué que normalement l'observateur status n'est pas le meilleur pour savoir vraiment quand l'objet AVPlayer est prêt à jouer, car l'objet peut être prêt à jouer, mais cela ne signifie pas qu'il sera joué immédiatement.

La meilleure idée pour le savoir est avec loadedTimeRanges.

Pour inscrire l'observateur

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

Écoutez l'observateur

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

Pour supprimer l'observateur (dealloc, viewWillDissapear ou avant d'enregistrer l'observateur), c'est un bon endroit pour appeler

- (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
    }
}
10
jose920405
private var playbackLikelyToKeepUpContext = 0

Pour enregistrer l'observateur

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

Écoutez l'observateur

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

Pour supprimer l'observateur

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

Le point clé du code est la propriété d'instance isPlaybackLikelyToKeepUp.

6
Harman

Basé sur réponse Tim Camber , voici la fonction Swift que j'utilise:

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
}

Ou, comme une extension

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
Axel Guilmin

J'ai eu des problèmes pour ne pas recevoir de rappels.

Il s'avère que cela dépend de la façon dont vous créez le flux. Dans mon cas, j'ai utilisé un playerItem pour initialiser, et j'ai donc dû ajouter l'observateur à l'élément à la place.

Par exemple:

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

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!!
        }
}
1
Alessandro Ornano

Vérifiez le statut de l'élément courant du joueur:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
1
Kirby Todd