Saber cuándo el objeto AVPlayer está listo para reproducirse

79

Estoy tratando de reproducir un MP3archivo que se pasa a un UIViewanterior UIView(almacenado en una NSURL *fileURLvariable).

Estoy inicializando un AVPlayercon:

player = [AVPlayer playerWithURL:fileURL];

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

Las NSLogimpresiones Player created:0,que imaginé significan que aún no está listo para jugar.

Cuando hago clic en reproducir UIButton, el código que ejecuto es:

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

}

Cuando ejecuto esto, siempre entro Error in player??en la consola. Sin embargo, si sustituyo la ifcondición que comprueba si AVPlayerestá listo para reproducirse, con un simple if(!isPlaying)..., la música se reproduce la SEGUNDA VEZ que hago clic en reproducir UIButton.

El registro de la consola es:

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

Veo que la SEGUNDA VEZ player.statusparece contener 1, que supongo que es AVPlayerReadyToPlay.

¿Qué puedo hacer para que la reproducción funcione correctamente la primera vez que hago clic en la reproducción UIButton? (es decir, ¿cómo puedo asegurarme de que AVPlayerno solo se haya creado, sino que también esté listo para jugar?)

mvishnu
fuente

Respuestas:

127

Está reproduciendo un archivo remoto. Puede tomar algún tiempo para que el AVPlayerbúfer de datos suficientes y esté listo para reproducir el archivo (consulte la Guía de programación de AV Foundation )

Pero parece que no esperas a que el reproductor esté listo antes de tocar el botón de reproducción. Lo que haría es deshabilitar este botón y habilitarlo solo cuando el reproductor esté listo.

Con KVO, es posible recibir notificaciones de cambios en el estado del jugador:

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

Este método se llamará cuando cambie el estado:

- (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
fuente
¡¡Gracias!! Eso funcionó a las mil maravillas. (debería haberlo adivinado cuando vi que estaba reproduciendo archivos sin conexión sin ningún problema)
mvishnu
Hay algunas URL que simplemente no se reproducen, existen pero no funcionan (por ejemplo, iTunes tampoco las reproducirá). ¿Cómo manejas ese comportamiento? No hay tiempo de espera en AVPlayer.
Fabrizio
10
En mi experiencia player.currentItem.statuses exacto cuando player.statusno lo es. No estoy seguro de cuáles son las diferencias.
bendytree
1
@iOSAppDev En IOS7 use AVPlayerItem addObserver
Peter Zhao
4
wow, este AVPlayer está tan mal diseñado que me hace llorar. ¿Por qué no agregar un bloque de controlador onLoad? ¡Vamos Apple, simplifica tus cosas!
Duck
30

Tuve muchos problemas para tratar de averiguar el estado de un AVPlayer. La statuspropiedad no siempre pareció ser muy útil, y esto me llevó a una frustración infinita cuando intentaba manejar las interrupciones de la sesión de audio. A veces AVPlayerme decían que estaba listo para jugar AVPlayerStatusReadyToPlaycuando en realidad no parecía estarlo. Usé el método KVO de Jilouc, pero no funcionó en todos los casos.

Para complementar, cuando la propiedad de estado no estaba siendo útil, consulté la cantidad de flujo que el AVPlayer había cargado mirando la loadedTimeRangespropiedad de AVPlayer's currentItem(que es an AVPlayerItem).

Todo es un poco confuso, pero así es como se ve:

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
fuente
2
Hay adiciones al tipo NSValue que vienen con AV Foundation. Algunos de esos ayudantes le permiten convertir de NSValue a valores CMTimeXxx de ida y vuelta. Como CMTimeRangeValue .
superjos
Historia similar para obtener segundos (supongo que eso timeLoadedes lo que es) de CMTime: CMTimeGetSeconds
superjos
2
Desafortunadamente, esta debería ser una respuesta aceptada. AVPlayerparece establecerse status == AVPlayerStatusReadyToPlaydemasiado pronto cuando no está listo para jugar realmente. Para que esto funcione, puede envolver el código anterior en la NSTimerinvocación, por ejemplo.
maxkonovalov
¿Podría ser el caso en el que hay más de (wlog) 2 segundos de rango de tiempo cargado, pero el estado del jugador o playerItem no es ReadyToPlay? IOW, ¿debería confirmarse también?
danielhadar
29

Solución rápida

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

También puede invalidar al observador de esta manera.

self.observer.invalidate()

Importante: Debe mantener la variable de observador retenida, de lo contrario, se desasignará y ya no se llamará al changeHandler. Por lo tanto, no defina al observador como una variable de función, sino defínalo como una variable de instancia como en el ejemplo dado.

Esta sintaxis de observador de valores clave es nueva en Swift 4.

Para obtener más información, consulte aquí https://github.com/ole/whats-new-in-swift-4/blob/master/Whats-new-in-Swift-4.playground/Pages/Key%20paths.xcplaygroundpage/ Contenido.swift

Josh Bernfeld
fuente
Gracias, este método es muy simple para eliminar KVO.
ZAFAR007
11

Después de investigar mucho y probar muchas formas, he notado que normalmente el statusobservador no es el mejor para saber realmente cuándo el AVPlayerobjeto está listo para jugar , porque el objeto puede estar listo para jugar, pero esto no significa que se jugará inmediatamente.

La mejor idea para saber esto es con loadedTimeRanges.

Para el observador de registro

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

Escucha al observador

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

Para eliminar observador (dealloc, viewWillDissapear o antes de registrar observador) es un buen lugar para llamar

- (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
fuente
gracias, esto funcionó para mí también. Sin embargo, no utilicé la evaluación "currentBufferDuration == seconds". ¿Podría decirme para qué se utiliza?
andrei
Para los casos en quecurrentBufferDuration < 2
jose920405
¿Podría ser el caso en el que hay más de (wlog) 2 segundos de rango de tiempo cargado, pero el estado del jugador o playerItem no es ReadyToPlay? IOW, ¿debería confirmarse también?
danielhadar
11
private var playbackLikelyToKeepUpContext = 0

Para registrar observador

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

Escucha al observador

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

Para quitar observador

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

El punto clave en el código es la propiedad de instancia isPlaybackLikelyToKeepUp.

Harman
fuente
3
Buena respuesta. Mejoraría KVO conforKeyPath: #keyPath(AVPlayer.currentItem.isPlaybackLikelyToKeepUp)
Miroslav Hrivik
Para 2019, esto funciona perfectamente : copie y pegue :) Usé el mod de @MiroslavHrivik, ¡gracias!
Fattie
7

Según la respuesta de Tim Camber , aquí está la función Swift que uso:

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
}

O, como una extensión

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
fuente
Con la extensión, supongo que no es posible que KVO observe la propiedad ready. ¿De alguna manera?
Jonny
Escucho las notificaciones AVPlayerItemNewAccessLogEntryy AVPlayerItemDidPlayToEndTimeen mi proyecto. Afaik funciona.
Axel Guilmin
OK, terminé escuchando loadedTimeRanges.
Jonny
5

Tuve problemas al no recibir ninguna devolución de llamada.

Resulta que depende de cómo crees la transmisión. En mi caso, utilicé un playerItem para inicializar y, por lo tanto, tuve que agregar el observador al elemento.

Por ejemplo:

- (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
fuente
¿El si no debería estar con AVPlayerItemStatusReadyToPlay?
jose920405
@ jose920405 Puedo confirmar que la solución anterior funciona, pero es una buena pregunta. Realmente no lo sé. Avísame si lo pruebas.
dac2009
3

Compruebe el estado del elemento actual del jugador:

if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)
Kirby Todd
fuente
2
player.currentItem.status devuelve AVPlayerItemStatusUnkown. No sé qué hacer a continuación. :(
mvishnu
Inicialmente este valor es AVPlayerItemStatusUnkown. Solo después de algún tiempo, podrá saber si es AVPlayerItemStatusReadyToPlayoAVPlayerItemStatusFailed
Gustavo Barbosa
2

Rápido 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
fuente
1

La respuesta de @JoshBernfeld no funcionó para mí. No estoy seguro de por qué. Observó playerItem.observe(\.status. Tuve que observar player?.observe(\.currentItem?.status. Parece que son lo mismo, la playerItem statuspropiedad.

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

cuando haya terminado de usar el reproductor playerStatusObserver = nil

Lance Samaria
fuente