Dans mon application, je dois lire des fichiers audio stockés sur un serveur Web. J'utilise AVPlayer
pour cela. J'ai tous les contrôles de lecture/pause et tous les délégués et observateurs qui fonctionnent parfaitement. Lors de la lecture de petits fichiers audio, tout fonctionne très bien.
Lorsqu'un long fichier audio est lu, il commence également à jouer correctement mais après quelques secondes, le AVPlayer
interrompt la lecture (probablement pour le mettre en mémoire tampon). Le problème est qu'elle ne reprend pas d'elle-même. Il reste dans un état de pause et si j'appuie à nouveau manuellement sur le bouton de lecture, il joue à nouveau en douceur.
Je veux savoir pourquoi AVPlayer
ne reprend pas automatiquement et comment puis-je réussir à reprendre l'audio sans que l'utilisateur n'appuie à nouveau sur le bouton de lecture? Merci.
Oui, il s'arrête car le tampon est vide, il doit donc attendre pour charger plus de vidéo. Après cela, vous devez demander manuellement pour recommencer. Pour résoudre le problème, j'ai suivi ces étapes:
1) Détection: Pour détecter quand le joueur s'est arrêté, j'utilise le KVO avec la propriété rate de la valeur:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"rate"] )
{
if (self.player.rate == 0 && CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) && self.videoPlaying)
{
[self continuePlaying];
}
}
}
Cette condition: CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime)
est de détecter la différence entre arriver à la fin de la vidéo ou s'arrêter au milieu
2) Attendez que la vidéo se charge - Si vous continuez à jouer directement, vous n'aurez pas assez de tampon pour continuer à jouer sans interruption. Pour savoir quand commencer il faut observer la valeur playbackLikelytoKeepUp
du playerItem (ici j'utilise une bibliothèque pour observer avec des blocs mais je pense que ça fait le point):
-(void)continuePlaying
{
if (!self.playerItem.playbackLikelyToKeepUp)
{
self.loadingView.hidden = NO;
__weak typeof(self) wSelf = self;
self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
__strong typeof(self) sSelf = wSelf;
if(sSelf)
{
if (sSelf.playerItem.playbackLikelyToKeepUp)
{
[sSelf.playerItem removeObserverForKeyPath:@keypath(_playerItem.playbackLikelyToKeepUp) token:self.playbackLikelyToKeepUpKVOToken];
sSelf.playbackLikelyToKeepUpKVOToken = nil;
[sSelf continuePlaying];
}
}
}];
}
Et c'est tout! problème résolu
Edit: Par la façon dont la bibliothèque utilisée est libextobjc
Je travaille avec des fichiers vidéo, donc mon code contient plus que ce dont vous avez besoin, mais la solution suivante devrait mettre le lecteur en pause lorsqu'il se bloque, puis vérifier toutes les 0,5 seconde pour voir si nous avons suffisamment de mémoire tampon pour suivre. Si c'est le cas, il redémarre le lecteur. Si le joueur se bloque pendant plus de 10 secondes sans redémarrer, nous arrêtons le joueur et nous nous excusons auprès de l'utilisateur. Cela signifie que vous avez besoin des bons observateurs en place. Le code ci-dessous fonctionne assez bien pour moi.
propriétés définies/initiées dans un fichier .h ou ailleurs:
AVPlayer *player;
int playerTryCount = -1; // this should get set to 0 when the AVPlayer starts playing
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
partiel .m:
- (AVPlayer *)initializePlayerFromURL:(NSURL *)movieURL {
// create AVPlayer
AVPlayerItem *videoItem = [AVPlayerItem playerItemWithURL:movieURL];
AVPlayer *videoPlayer = [AVPlayer playerWithPlayerItem:videoItem];
// add Observers
[videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
[self startNotificationObservers]; // see method below
// I observe a bunch of other stuff, but this is all you need for this to work
return videoPlayer;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
if (self.player.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.player.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.player.currentTime, !=, self.player.currentItem.duration)) {
// if so, post the playerHanging notification
[self.notificationCenter postNotificationName:PlayerHangingNotification object:self.videoPlayer];
}
}
}
- (void)startNotificationObservers {
[self.notificationCenter addObserver:self
selector:@selector(playerContinue)
name:PlayerContinueNotification
object:nil];
[self.notificationCenter addObserver:self
selector:@selector(playerHanging)
name:PlayerHangingNotification
object:nil];
}
// playerHanging simply decides whether to wait 0.5 seconds or not
// if so, it pauses the player and sends a playerContinue notification
// if not, it puts us out of our misery
- (void)playerHanging {
if (playerTryCount <= 10) {
playerTryCount += 1;
[self.player pause];
// start an activity indicator / busy view
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.player];
} else { // this code shouldn't actually execute, but I include it as dummyproofing
[self stopPlaying]; // a method where I clean up the AVPlayer,
// which is already paused
// Here's where I'd put up an alertController or alertView
// to say we're sorry but we just can't go on like this anymore
}
}
// playerContinue does the actual waiting and restarting
- (void)playerContinue {
if (CMTIME_COMPARE_INLINE(self.player.currentTime, ==, self.player.currentItem.duration)) { // we've reached the end
[self stopPlaying];
} else if (playerTryCount > 10) // stop trying
[self stopPlaying];
// put up "sorry" alert
} else if (playerTryCount == 0) {
return; // protects against a race condition
} else if (self.player.currentItem.playbackLikelyToKeepUp == YES) {
// Here I stop/remove the activity indicator I put up in playerHanging
playerTryCount = 0;
[self.player play]; // continue from where we left off
} else { // still hanging, not at end
// create a 0.5-second delay to see if buffering catches up
// then post another playerContinue notification to call this method again
// in a manner that attempts to avoid any recursion or threading nightmares
playerTryCount += 1;
double delayInSeconds = 0.5;
dispatch_time_t executeTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(executeTime, dispatch_get_main_queue(), ^{
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if (playerTryCount > 0) {
if (playerTryCount <= 10) {
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.videoPlayer];
} else {
[self stopPlaying];
// put up "sorry" alert
}
}
});
}
J'espère que cela aide!
J'ai eu un problème similaire. J'avais des fichiers locaux que je voulais lire, configuré l'AVPlayer et appelé [player play], le joueur s'arrête à l'image 0 et ne jouait plus tant que je n'avais pas appelé play à nouveau manuellement. La réponse acceptée était impossible pour moi à mettre en œuvre en raison d'une explication erronée, alors j'ai juste essayé de retarder le jeu et j'ai travaillé comme par magie
[self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];
-(void)startVideo{
[self.videoPlayer play];
}
Pour les vidéos Web, j'ai également eu le problème, je le résous en utilisant la réponse de Wallace.
Lors de la création de l'AVPlayer, ajoutez un observateur:
[self.videoItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {
if (self.videoPlayer.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, !=, self.videoPlayer.currentItem.duration)) {
NSLog(@"hanged");
[self performSelector:@selector(startVideo) withObject:nil afterDelay:0.2];
}
}
}
N'oubliez pas de supprimer l'observateur avant de fermer la vue
[self.videoItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"]
La réponse acceptée donne une solution possible au problème, mais elle manque de flexibilité, elle est également difficile à lire. Voici une solution plus flexible.
Ajouter des observateurs:
//_player is instance of AVPlayer
[_player.currentItem addObserver:self forKeyPath:@"status" options:0 context:nil];
[_player addObserver:self forKeyPath:@"rate" options:0 context:nil];
Gestionnaire:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:@"status"]) {
if (_player.status == AVPlayerStatusFailed) {
//Possibly show error message or attempt replay from tart
//Description from the docs:
// Indicates that the player can no longer play AVPlayerItem instances because of an error. The error is described by
// the value of the player's error property.
}
}else if ([keyPath isEqualToString:@"rate"]) {
if (_player.rate == 0 && //if player rate dropped to 0
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
_isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
[self handleStalled];
}
}
}
La magie:
-(void)handleStalled {
NSLog(@"Handle stalled. Available: %lf", [self availableDuration]);
if (_player.currentItem.playbackLikelyToKeepUp || //
[self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
[_player play];
} else {
[self performSelector:@selector(handleStalled) withObject:nil afterDelay:0.5]; //try again
}
}
"[Self availableDuration]" est facultatif, mais vous pouvez lancer manuellement la lecture en fonction de la quantité de vidéo disponible. Vous pouvez modifier la fréquence à laquelle le code vérifie si suffisamment de vidéo est mise en mémoire tampon. Si vous décidez d'utiliser la partie facultative, voici l'implémentation de la méthode:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
N'oubliez pas le nettoyage. Supprimer des observateurs:
[_player.currentItem removeObserver:self forKeyPath:@"status"];
[_player removeObserver:self forKeyPath:@"rate"];
Et possibles appels en attente pour gérer la vidéo bloquée:
[UIView cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleStalled) object:nil];
Je pense que l'utilisation de AVPlayerItemPlaybackStalledNotification
pour détecter le blocage est une meilleure façon.
D'abord, j'observe pour la lecture au point mort
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerStalled),
name: AVPlayerItemPlaybackStalledNotification, object: videoPlayer.currentItem)
Ensuite, je force la poursuite de la lecture
func playerStalled(note: NSNotification) {
let playerItem = note.object as! AVPlayerItem
if let player = playerItem.valueForKey("player") as? AVPlayer{
player.play()
}
}
Ce n'est probablement pas la meilleure façon de le faire, mais je l'utilise jusqu'à ce que je trouve quelque chose de mieux :)
dans un très mauvais réseau playbackLikelyToKeepUp
très probablement faux.
utilisez kvo
pour observer playbackBufferEmpty
est meilleur, plus sensible aux données de tampon existantes qui peuvent être utilisées pour la lecture .si la valeur passe à true, vous pouvez appeler la méthode de lecture pour continuer la lecture.