Boucler une vidéo avec AVFoundation AVPlayer?

142

Existe-t-il un moyen relativement simple de boucler une vidéo dans AVFoundation?

J'ai créé mon AVPlayer et AVPlayerLayer comme ceci:

avPlayer = [[AVPlayer playerWithURL:videoUrl] retain];
avPlayerLayer = [[AVPlayerLayer playerLayerWithPlayer:avPlayer] retain];

avPlayerLayer.frame = contentView.layer.bounds;
[contentView.layer addSublayer: avPlayerLayer];

puis je joue ma vidéo avec:

[avPlayer play];

La vidéo est bien lue mais s'arrête à la fin. Avec MPMoviePlayerController, tout ce que vous avez à faire est de définir sa repeatModepropriété sur la bonne valeur. Il ne semble pas y avoir de propriété similaire sur AVPlayer. Il ne semble pas non plus y avoir de rappel qui me dira quand le film est terminé pour que je puisse chercher au début et le rejouer.

Je n'utilise pas MPMoviePlayerController car il présente de sérieuses limitations. Je souhaite pouvoir lire plusieurs flux vidéo à la fois.

orj
la source
1
Voir cette réponse pour un lien vers le code de travail réel: stackoverflow.com/questions/7822808/…
MoDJ

Réponses:

275

Vous pouvez recevoir une notification lorsque le joueur se termine. VérifierAVPlayerItemDidPlayToEndTimeNotification

Lors de la configuration du lecteur:

ObjC

  avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; 

  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(playerItemDidReachEnd:)
                                               name:AVPlayerItemDidPlayToEndTimeNotification
                                             object:[avPlayer currentItem]];

cela empêchera le lecteur de faire une pause à la fin.

dans la notification:

- (void)playerItemDidReachEnd:(NSNotification *)notification {
    AVPlayerItem *p = [notification object];
    [p seekToTime:kCMTimeZero];
}

cela rembobinera le film.

N'oubliez pas de désenregistrer la notification lors de la libération du lecteur.

Rapide

avPlayer?.actionAtItemEnd = .none

NotificationCenter.default.addObserver(self,
                                       selector: #selector(playerItemDidReachEnd(notification:)),
                                       name: .AVPlayerItemDidPlayToEndTime,
                                       object: avPlayer?.currentItem)

@objc func playerItemDidReachEnd(notification: Notification) {
    if let playerItem = notification.object as? AVPlayerItem {
        playerItem.seek(to: kCMTimeZero)
    }
}

Swift 4+

@objc func playerItemDidReachEnd(notification: Notification) {
    if let playerItem = notification.object as? AVPlayerItem {
        playerItem.seek(to: CMTime.zero, completionHandler: nil)
    }
}
Bastian
la source
6
... et si vous voulez le lire juste après [p seekToTime: kCMTimeZero] (une sorte de "rembobinage"), recommencez simplement [p play].
thomax
24
cela ne devrait pas être nécessaire ... si vous le faites, avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;cela ne s'arrêtera pas, donc pas besoin de le remettre en jeu
Bastian
Pour faire jouer à nouveau le son, vous devez appeler [player play];après le rembobinage.
Kenny Winker
12
Cette solution fonctionne, mais elle n'est pas totalement transparente. J'ai une toute petite pause. Est-ce que je fais quelque chose de mal?
Joris van Liempd iDeveloper
3
@Praxiteles vous devez le désenregistrer lorsque la vue est détruite, ou lorsque vous supprimez le lecteur vidéo ou quoi que vous fassiez,) Vous pouvez utiliser [[NSNotificationCenter defaultCenter] removeObserver:self];par exemple, quand selfécoute les notifications.
Bastian
63

Si cela vous aide, dans iOS / tvOS 10, il existe un nouvel AVPlayerLooper () que vous pouvez utiliser pour créer une boucle transparente de la vidéo (Swift):

player = AVQueuePlayer()
playerLayer = AVPlayerLayer(player: player)
playerItem = AVPlayerItem(url: videoURL)
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
player.play()    

Cela a été présenté à la WWDC 2016 dans "Advances in AVFoundation Playback": https://developer.apple.com/videos/play/wwdc2016/503/

Même en utilisant ce code, j'ai eu un hoquet jusqu'à ce que je dépose un rapport de bogue avec Apple et que j'obtienne cette réponse:

Le fichier vidéo dont la durée est supérieure à celle des pistes audio / vidéo est le problème. FigPlayer_File désactive la transition sans interruption car le montage de la piste audio est plus court que la durée du film (15.682 vs 15.787).

Vous devez soit corriger les fichiers vidéo pour que la durée du film et les durées des pistes soient identiques, soit utiliser le paramètre de plage de temps d'AVPlayerLooper (définir la plage de temps de 0 à la durée de la piste audio)

Il s'avère que Premiere exportait des fichiers avec une piste audio d'une longueur légèrement différente de celle de la vidéo. Dans mon cas, il était bien de supprimer complètement l'audio, et cela a résolu le problème.

Nabha
la source
2
Rien d'autre n'a fonctionné pour moi. J'utilise un AVPlayerLooper et j'ai eu ce bogue et la correction de l'écart entre les longueurs vidéo / audio a résolu le problème.
Kevin Heap
1
Merci pour ces informations sur Premiere. J'ai ajouté un timeRange au looper et cela a résolu mon problème de "vidéo clignotante".
Alexander Flenniken
@Nabha est-il possible de l'utiliser pendant un certain laps de temps dans la vidéo? Par exemple, la vidéo dure 60 secondes, mais je veux boucler les 10 premières secondes uniquement
Lance Samaria
29

Dans Swift :

Vous pouvez obtenir une notification lorsque le lecteur se termine ... vérifiez AVPlayerItemDidPlayToEndTimeNotification

lors de la configuration du lecteur:

avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None

NSNotificationCenter.defaultCenter().addObserver(self, 
                                                 selector: "playerItemDidReachEnd:", 
                                                 name: AVPlayerItemDidPlayToEndTimeNotification, 
                                                 object: avPlayer.currentItem)

cela empêchera le lecteur de faire une pause à la fin.

dans la notification:

func playerItemDidReachEnd(notification: NSNotification) {
    if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
        playerItem.seekToTime(kCMTimeZero)
    }
}

Swift3

NotificationCenter.default.addObserver(self,
    selector: #selector(PlaylistViewController.playerItemDidReachEnd),
     name: NSNotification.Name.AVPlayerItemDidPlayToEndTime,
     object: avPlayer?.currentItem)

cela rembobinera le film.

N'oubliez pas de désenregistrer la notification lors de la libération du lecteur.

Roi-sorcier
la source
4
Je vois un petit hoquet entre les boucles avec cette méthode. J'ai ouvert ma vidéo dans Adobe Premier et vérifié qu'il n'y avait pas d'images en double dans la vidéo, donc le bref hoquet est définitivement à la lecture. Quelqu'un a-t-il trouvé un moyen de créer une boucle vidéo de manière transparente et rapide?
SpaceManGalaxy
@SpaceManGalaxy J'ai également remarqué le hoquet. Avez-vous trouvé un moyen de résoudre ce problème?
Lance Samaria
18

Voici ce que j'ai fini par faire pour éviter le problème de pause-hoquet:

Rapide:

NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime,
                                       object: nil,
                                       queue: nil) { [weak self] note in
                                        self?.avPlayer.seek(to: kCMTimeZero)
                                        self?.avPlayer.play()
}

Objectif c:

__weak typeof(self) weakSelf = self; // prevent memory cycle
NSNotificationCenter *noteCenter = [NSNotificationCenter defaultCenter];
[noteCenter addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
                        object:nil
                         queue:nil
                    usingBlock:^(NSNotification *note) {
                        [weakSelf.avPlayer seekToTime:kCMTimeZero];
                        [weakSelf.avPlayer play];
                    }];

REMARQUE: je n'ai pas utilisé avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNonecar ce n'est pas nécessaire.

L'Islam Q.
la source
2
@KostiaDombrovsky avez-vous essayé sur un appareil réel ou sur différentes vidéos?
Islam Q.
@IslamQ. J'enregistre un fichier MP4, puis j'essaye de le lire en boucle un peu comme le fait Snapchat.
Kostia Dombrovsky
@KostiaDombrovsky avez-vous comparé votre lecture avec Snapchat côte à côte? Je pense que parce que les cadres de début et de fin ne correspondent pas, il semble que cela ait été mis en pause, mais cela ne s'arrête jamais.
Islam Q.
Cela n'a pas fonctionné pour moi non plus. J'ai une vidéo de 6 secondes avec un son incessant et j'entends une fraction de seconde de silence avec cette méthode
Cbas
Je vois une fuite de mémoire lors de l'utilisation de cette approche. Cela a à voir avec les [weakSelf.avPlayer seekToTime:kCMTimeZero]; [weakSelf.avPlayer play];lignes - lorsque je commente ces lignes, il n'y a plus de fuite de mémoire. J'ai profilé cela dans les instruments.
Solsma Dev
3

Je recommande d'utiliser AVQueuePlayer pour boucler vos vidéos de manière transparente. Ajouter l'observateur de notification

AVPlayerItemDidPlayToEndTimeNotification

et dans son sélecteur, bouclez votre vidéo

AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];
kevinnguy
la source
J'ai essayé cela et cela ne montre aucune amélioration par rapport à la méthode suggérée par @Bastian. Avez-vous réussi à supprimer totalement le hoquet avec ça?
amadour le
2
@amadour ce que vous pouvez faire est d'ajouter 2 des mêmes vidéos dans le lecteur AVQueuePlayer une fois initialisé et lorsque le lecteur publie la notification AVPlayerItemDidPlayToEndTimeNotification, ajoutez la même vidéo à la file d'attente du lecteur.
kevinnguy
3

Pour éviter l'écart lorsque la vidéo est rembobinée, l'utilisation de plusieurs copies du même élément dans une composition a bien fonctionné pour moi. Je l'ai trouvé ici: www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html (lien maintenant mort).

AVURLAsset *tAsset = [AVURLAsset assetWithURL:tURL];
CMTimeRange tEditRange = CMTimeRangeMake(CMTimeMake(0, 1), CMTimeMake(tAsset.duration.value, tAsset.duration.timescale));
AVMutableComposition *tComposition = [[[AVMutableComposition alloc] init] autorelease];
for (int i = 0; i < 100; i++) { // Insert some copies.
    [tComposition insertTimeRange:tEditRange ofAsset:tAsset atTime:tComposition.duration error:nil];
}
AVPlayerItem *tAVPlayerItem = [[AVPlayerItem alloc] initWithAsset:tComposition];
AVPlayer *tAVPlayer = [[AVPlayer alloc] initWithPlayerItem:tAVPlayerItem];
user2581875
la source
Je suppose que vous voulez dire ce lien devbrief.blogspot.se/2011/12/…
flame3
2

cela a fonctionné pour moi sans problème de hoquet, le point est de mettre le lecteur en pause avant d'appeler la méthode seekToTime:

  1. init AVPlayer

    let url = NSBundle.mainBundle().URLForResource("loop", withExtension: "mp4")
    let playerItem = AVPlayerItem(URL: url!)
    
    self.backgroundPlayer = AVPlayer(playerItem: playerItem)
    let playerLayer = AVPlayerLayer(player: self.backgroundPlayer)
    
    playerLayer.frame = CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height)
    self.layer.addSublayer(playerLayer)
    self.backgroundPlayer!.actionAtItemEnd = .None
    self.backgroundPlayer!.play()
  2. enregistrement de la notification

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoLoop", name: AVPlayerItemDidPlayToEndTimeNotification, object: self.backgroundPlayer!.currentItem)
  3. fonction videoLoop

    func videoLoop() {
      self.backgroundPlayer?.pause()
      self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero)
      self.backgroundPlayer?.play()
    }
Vojta
la source
3
Merci - j'ai essayé cela, mais il y a encore une pause pour moi.
Nabha
2

Pour Swift 3 & 4

NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.avPlayer?.currentItem, queue: .main) { _ in
     self.avPlayer?.seek(to: kCMTimeZero)
     self.avPlayer?.play()
}
vp2698
la source
1

ma solution en objective-c avec AVQueuePlayer - il semble que vous deviez dupliquer l'AVPlayerItem et à la fin de la lecture du premier élément, ajoutez instantanément une autre copie. "Kind of" a du sens et fonctionne pour moi sans aucun hoquet

NSURL *videoLoopUrl; 
// as [[NSBundle mainBundle] URLForResource:@"assets/yourVideo" withExtension:@"mp4"]];
AVQueuePlayer *_loopVideoPlayer;

+(void) nextVideoInstance:(NSNotification*)notif
{
 AVPlayerItem *currItem = [AVPlayerItem playerItemWithURL: videoLoopUrl];

[[NSNotificationCenter defaultCenter] addObserver:self
                                      selector:@selector(nextVideoInstance:)
                                      name:AVPlayerItemDidPlayToEndTimeNotification
                                      object: currItem];

 [_loopVideoPlayer insertItem:currItem afterItem:nil];
 [_loopVideoPlayer advanceToNextItem];

}

+(void) initVideoPlayer {
 videoCopy1 = [AVPlayerItem playerItemWithURL: videoLoopUrl];
 videoCopy2 = [AVPlayerItem playerItemWithURL: videoLoopUrl];
 NSArray <AVPlayerItem *> *dummyArray = [NSArray arrayWithObjects: videoCopy1, videoCopy2, nil];
 _loopVideoPlayer = [AVQueuePlayer queuePlayerWithItems: dummyArray];

 [[NSNotificationCenter defaultCenter] addObserver: self
                                      selector: @selector(nextVideoInstance:)
                                      name: AVPlayerItemDidPlayToEndTimeNotification
                                      object: videoCopy1];

 [[NSNotificationCenter defaultCenter] addObserver: self
                                      selector: @selector(nextVideoInstance:)
                                      name: AVPlayerItemDidPlayToEndTimeNotification
                                      object: videoCopy2];
}

https://gist.github.com/neonm3/06c3b5c911fdd3ca7c7800dccf7202ad

néon M3
la source
1

Swift 5:

J'ai fait quelques légers ajustements par rapport aux réponses précédentes, comme l'ajout du playerItem à la file d'attente avant de l'ajouter au playerLayer.

let playerItem = AVPlayerItem(url: url)
let player = AVQueuePlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)

playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)

playerLayer.frame = cell.eventImage.bounds
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill

// Add the playerLayer to a UIView.layer

player.play()

Et faites de playerLooper une propriété de votre UIViewController, sinon la vidéo ne peut être lue qu'une seule fois.

NSCoder
la source
0

Après avoir chargé la vidéo dans l'AVPlayer (via son AVPlayerItem, bien sûr):

 [self addDidPlayToEndTimeNotificationForPlayerItem:item];

La méthode addDidPlayToEndTimeNotificationForPlayerItem:

- (void)addDidPlayToEndTimeNotificationForPlayerItem:(AVPlayerItem *)item
{
    if (_notificationToken)
        _notificationToken = nil;

    /*
     Setting actionAtItemEnd to None prevents the movie from getting paused at item end. A very simplistic, and not gapless, looped playback.
     */
    _player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
    _notificationToken = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:item queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
        // Simple item playback rewind.
        [[_player currentItem] seekToTime:kCMTimeZero];
    }];
}

Dans votre méthode viewWillDisappear:

if (_notificationToken) {
        [[NSNotificationCenter defaultCenter] removeObserver:_notificationToken name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];
        _notificationToken = nil;
    }

Dans la déclaration d'interface de votre contrôleur de vue dans le fichier d'implémentation:

id _notificationToken;

Besoin de voir cela opérationnel avant d'essayer? Téléchargez et exécutez cet exemple d'application:

https://developer.apple.com/library/prerelease/ios/samplecode/AVBasicVideoOutput/Listings/AVBasicVideoOutput_APLViewController_m.html#//apple_ref/doc/uid/DTS40013109-AVBasicVideoOutput_Output_APLViewController_m.html#//apple_ref/doc/uid/DTS40013109-AVBasicVideoOutput_Output_APLViewController_m.html#//apple_ref/doc/uid/DTS40013109-AVBasicVideoOutput_French_IDController8D

Dans mon application, qui utilise ce code même, il n'y a aucune pause entre la fin de la vidéo et le début. En fait, en fonction de la vidéo, il n'y a aucun moyen pour moi de dire que la vidéo est à nouveau au début, enregistrez l'affichage du code temporel.

James Bush
la source
0

vous pouvez ajouter un observateur AVPlayerItemDidPlayToEndTimeNotification et rejouer la vidéo depuis le début dans le sélecteur, code comme ci-dessous

 //add observer
[[NSNotificationCenter defaultCenter] addObserver:self                                                 selector:@selector(playbackFinished:)                                                     name:AVPlayerItemDidPlayToEndTimeNotification
object:_aniPlayer.currentItem];

-(void)playbackFinished:(NSNotification *)notification{
    [_aniPlayer seekToTime:CMTimeMake(0, 1)];//replay from start
    [_aniPlayer play];
}
shujucn
la source
0

Ce qui suit fonctionne pour moi dans WKWebView dans swift 4.1 La partie principale de WKWebView dans WKwebviewConfiguration

wkwebView.navigationDelegate = self
wkwebView.allowsBackForwardNavigationGestures = true
self.wkwebView =  WKWebView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height))
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
wkwebView = WKWebView(frame: wkwebView.frame, configuration: config)
self.view.addSubview(wkwebView)
self.wkwebView.load(NSURLRequest(url: URL(string: self.getUrl())!) as URLRequest)
Nrv
la source
0

Ce que j'ai fait, c'est de le faire jouer en boucle, comme mon code ci-dessous:

[player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0)
queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    float current = CMTimeGetSeconds(time);
    float total = CMTimeGetSeconds([playerItem duration]);
    if (current >= total) {
        [[self.player currentItem] seekToTime:kCMTimeZero];
        [self.player play];
    }
}];
Johnny Zhang
la source
0

Swift 4.2 dans Xcode 10.1.

Oui , il existe un moyen relativement simple de mettre en boucle une vidéo dans AVKit/ en AVFoundationutilisant la AVQueuePlayer()technique d'observation de la valeur clé (KVO) et un jeton pour cela.

Cela fonctionne certainement pour un tas de vidéos H.264 / HEVC avec une charge minimale pour le processeur.

Voici un code:

import UIKit
import AVFoundation
import AVKit

class ViewController: UIViewController {

    private let player = AVQueuePlayer()
    let clips = ["01", "02", "03", "04", "05", "06", "07"]
    private var token: NSKeyValueObservation?
    var avPlayerView = AVPlayerViewController()

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(true)

        self.addAllVideosToPlayer()
        present(avPlayerView, animated: true, completion: { self.player.play() })
    }

    func addAllVideosToPlayer() {
        avPlayerView.player = player

        for clip in clips {
            let urlPath = Bundle.main.path(forResource: clip, ofType: "m4v")!
            let url = URL(fileURLWithPath: urlPath)
            let playerItem = AVPlayerItem(url: url)
            player.insert(playerItem, after: player.items().last)

            token = player.observe(\.currentItem) { [weak self] player, _ in
                if self!.player.items().count == 1 { self?.addAllVideosToPlayer() }
            }
        }
    }
}
Andy Fedoroff
la source
-1

utilisez AVPlayerViewController ci-dessous le code, cela fonctionne pour moi

        let type : String! = "mp4"
        let targetURL : String? = NSBundle.mainBundle().pathForResource("Official Apple MacBook Air Video   YouTube", ofType: "mp4")

        let videoURL = NSURL(fileURLWithPath:targetURL!)


        let player = AVPlayer(URL: videoURL)
        let playerController = AVPlayerViewController()

        playerController.player = player
        self.addChildViewController(playerController)
        self.playView.addSubview(playerController.view)
        playerController.view.frame = playView.bounds

        player.play()

Tous les contrôles à montrer, j'espère que c'est utile

Iyyappan Ravi
la source
-2
/* "numberOfLoops" is the number of times that the sound will return to the beginning upon reaching the end. 
A value of zero means to play the sound just once.
A value of one will result in playing the sound twice, and so on..
Any negative number will loop indefinitely until stopped.
*/
@property NSInteger numberOfLoops;

Cette propriété est déjà définie à l'intérieur AVAudioPlayer. J'espère que cela peut vous aider. J'utilise Xcode 6.3.

Julz
la source
8
c'est pour l'audio, pas pour AVPlayer
Yariv Nissim