Looping um vídeo com o AVFoundation AVPlayer?

142

Existe uma maneira relativamente fácil de repetir um vídeo no AVFoundation?

Eu criei meu AVPlayer e AVPlayerLayer assim:

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

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

e depois reproduzo meu vídeo com:

[avPlayer play];

O vídeo é bom, mas para no final. Com o MPMoviePlayerController, tudo o que você precisa fazer é definir sua repeatModepropriedade para o valor certo. Não parece haver uma propriedade semelhante no AVPlayer. Também não parece haver um retorno de chamada que me dirá quando o filme terminar, para que eu possa procurar o começo e reproduzi-lo novamente.

Não estou usando o MPMoviePlayerController porque possui algumas limitações sérias. Quero poder reproduzir vários fluxos de vídeo de uma só vez.

orj
fonte
1
Veja esta resposta para obter um link para o código de trabalho real: stackoverflow.com/questions/7822808/…
MoDJ

Respostas:

275

Você pode receber uma notificação quando o jogador terminar. VerificaAVPlayerItemDidPlayToEndTimeNotification

Ao configurar o player:

ObjC

  avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; 

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

isso impedirá que o jogador faça uma pausa no final.

na notificação:

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

isso vai retroceder o filme.

Não se esqueça de cancelar o registro da notificação ao liberar o player.

Rápido

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
fonte
6
... e se você quiser reproduzi-lo logo após [p seekToTime: kCMTimeZero] (uma espécie de "rebobinar"), basta executar [p play] novamente.
Thomax
24
isso não deve ser necessário ... se você fizer avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;isso não vai parar, então não há necessidade de configurá-lo para jogar novamente
Bastian
Para fazer o som tocar novamente, você deve ligar [player play];após rebobinar.
precisa
12
Esta solução funciona, mas não é completamente perfeita. Eu tenho uma pausa muito pequena. Estou fazendo algo errado?
Joris van Liempd iDeveloper 21/12/12
3
@Praxiteles, é necessário cancelar o registro quando a exibição for destruída, ou quando você remover o vídeo ou o que fizer,) Você pode usar, [[NSNotificationCenter defaultCenter] removeObserver:self];por exemplo, quando selfestiver ouvindo as notificações.
Bastian
63

Se ajudar, no iOS / tvOS 10, há um novo AVPlayerLooper () que você pode usar para criar loop contínuo de vídeo (Swift):

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

Isso foi apresentado na WWDC 2016 em "Avanços na reprodução da AVFoundation": https://developer.apple.com/videos/play/wwdc2016/503/

Mesmo usando esse código, tive um soluço até registrar um bug na Apple e obter esta resposta:

O arquivo de filme com duração de filme maior que as faixas de áudio / vídeo é o problema. O FigPlayer_File está desativando a transição sem intervalos porque a edição da faixa de áudio é menor que a duração do filme (15.682 vs 15.787).

Você precisa corrigir os arquivos do filme para que a duração do filme e a duração da faixa tenham a mesma duração ou pode usar o parâmetro de intervalo de tempo do AVPlayerLooper (defina o intervalo de 0 a duração da faixa de áudio)

Acontece que o Premiere estava exportando arquivos com uma faixa de áudio com uma duração ligeiramente diferente do vídeo. No meu caso, foi bom remover completamente o áudio, e isso resolveu o problema.

Nabha
fonte
2
Nada mais funcionou para mim. Estou usando um AVPlayerLooper e tive esse bug e a correção da discrepância entre os comprimentos de vídeo / áudio resolveu o problema.
Kevin Heap
1
Obrigado por essas informações sobre o Premiere. Adicionei um timeRange ao looper e isso corrigiu o problema do "vídeo intermitente".
Alexander Flenniken
@ Nabha, é possível usar isso por um determinado período de tempo no vídeo? Por exemplo, o vídeo tem 60 segundos, mas quero repetir apenas os primeiros 10 segundos
Lance Samaria
29

Em Swift :

Você pode receber uma notificação quando o player terminar ... verifique AVPlayerItemDidPlayToEndTimeNotification

ao configurar o player:

avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None

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

isso impedirá que o jogador faça uma pausa no final.

na notificação:

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)

isso vai retroceder o filme.

Não se esqueça de cancelar o registro da notificação ao liberar o player.

O rei da bruxaria
fonte
4
Estou vendo um pequeno soluço entre os loops com esse método. Abri meu vídeo no Adobe Premier e verifiquei que não há quadros duplicados no vídeo, então o soluço breve está definitivamente em reprodução. Alguém encontrou uma maneira de fazer um loop de vídeo sem interrupções rapidamente?
SpaceManGalaxy #
@ SpaceManGalaxy Eu também notei o soluço. Você encontrou uma maneira de corrigir essa falha?
Lance Samaria
18

Aqui está o que acabei fazendo para evitar o problema da pausa-soluço:

Rápido:

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

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

NOTA: Não usei, avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNonepois não é necessário.

Islam Q.
fonte
2
@KostiaDombrovsky você experimentou um dispositivo real ou vídeos diferentes?
Islam Q.
@IslamQ. Gravo um arquivo MP4 e tento reproduzi-lo em um loop, como o Snapchat faz.
Kostia Dombrovsky
@KostiaDombrovsky você comparou sua reprodução com o snapchat lado a lado? Acho que porque os quadros inicial e final não coincidem, parece que ele foi pausado, mas nunca para.
Islam Q.
Também não funcionou para mim. Eu tenho um segundo vídeo 6 com áudio incessante e eu continuo a ouvir uma fração de segundo de silêncio com este método
ACB
Estou vendo um vazamento de memória ao usar essa abordagem. Tem a ver com as [weakSelf.avPlayer seekToTime:kCMTimeZero]; [weakSelf.avPlayer play];linhas - quando eu comento essas linhas, não há mais vazamento de memória. Eu perfilei isso em instrumentos.
Solsma Dev
3

Eu recomendo usar o AVQueuePlayer para repetir seus vídeos sem problemas. Adicione o observador de notificações

AVPlayerItemDidPlayToEndTimeNotification

e no seletor, faça um loop do seu vídeo

AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];
kevinnguy
fonte
Eu tentei isso e ele não mostra nenhuma melhoria em relação ao método sugerido por @Bastian. Você conseguiu remover totalmente o soluço com isso?
amadour 29/09/14
2
@amadour, o que você pode fazer é adicionar 2 dos mesmos vídeos no AVQueuePlayer player quando inicializado e quando o player postar o AVPlayerItemDidPlayToEndTimeNotification, adicione o mesmo vídeo à fila do player.
Kevinnguy
3

Para evitar a lacuna quando o vídeo é rebobinado, o uso de várias cópias do mesmo ativo em uma composição funcionou bem para mim. Encontrei-o aqui: www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html (link agora morto).

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
fonte
Eu acho que você quer dizer este link devbrief.blogspot.se/2011/12/…
flame3
2

isso funcionou para mim sem problemas de soluço, o ponto é pausar o player antes de chamar o método 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. registrando notificação

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "videoLoop", name: AVPlayerItemDidPlayToEndTimeNotification, object: self.backgroundPlayer!.currentItem)
  3. função videoLoop

    func videoLoop() {
      self.backgroundPlayer?.pause()
      self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero)
      self.backgroundPlayer?.play()
    }
Vojta
fonte
3
Obrigado - tentei isso, mas ainda há uma pausa para mim.
Nabha
2

Para Swift 3 e 4

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

minha solução no objetivo-c com AVQueuePlayer - parece que você precisa duplicar o AVPlayerItem e, ao terminar a reprodução do primeiro elemento, adicione instantaneamente outra cópia. "Mais ou menos" faz sentido e funciona para mim sem nenhum soluço

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

neon M3
fonte
1

Swift 5:

Fiz alguns pequenos ajustes nas respostas anteriores, como adicionar o playerItem à fila antes de adicioná-lo ao 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()

E torne o playerLooper uma propriedade do seu UIViewController, caso contrário, o vídeo poderá ser reproduzido apenas uma vez.

NSCoder
fonte
0

Depois de carregar o vídeo no AVPlayer (através do seu AVPlayerItem, é claro):

 [self addDidPlayToEndTimeNotificationForPlayerItem:item];

O método 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];
    }];
}

No seu método viewWillDisappear:

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

Na declaração da interface do seu controlador de exibição no arquivo de implementação:

id _notificationToken;

Precisa ver isso em funcionamento antes de tentar? Faça o download e execute este aplicativo de exemplo:

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

No meu aplicativo, que usa esse mesmo código, não há pausa entre o final do vídeo e o início. De fato, dependendo do vídeo, não há como dizer que o vídeo está no início novamente, salve a exibição do código de tempo.

James Bush
fonte
0

você pode adicionar um observador AVPlayerItemDidPlayToEndTimeNotification e reproduzir o vídeo do início no seletor, código como abaixo

 //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
fonte
0

O seguinte está funcionando para mim no WKWebView no swift 4.1 A parte principal do WKWebView no 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
fonte
0

O que fiz foi fazer a reprodução em loop, como no meu código abaixo:

[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
fonte
0

Swift 4.2 no Xcode 10.1.

Sim , existe uma maneira relativamente fácil de inserir um vídeo em AVKit/ AVFoundationusarAVQueuePlayer() técnica de Observação de Valor-Chave (KVO) e um token para ele.

Isso definitivamente funciona para vários vídeos H.264 / HEVC com uma carga mínima para a CPU.

Aqui está um código:

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
fonte
-1

use AVPlayerViewController abaixo do código, está funcionando para mim

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

Todos os controles a serem mostrados, espero que seja útil

Iyyappan Ravi
fonte
-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;

Esta propriedade já está definida por dentro AVAudioPlayer. Espero que isso possa ajudá-lo. Estou usando o Xcode 6.3.

Julz
fonte
8
Isso é para áudio, não para AVPlayer
Yariv Nissim