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?)
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
}
}
}
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
}
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
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
}
}
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.
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
}
}
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"];
}
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!!
}
}
Vérifiez le statut de l'élément courant du joueur:
if (player.currentItem.status == AVPlayerItemStatusReadyToPlay)