¿Reproducir un video en bucle con AVFoundation AVPlayer?

142

¿Hay una manera relativamente fácil de reproducir un video en AVFoundation?

He creado mi AVPlayer y AVPlayerLayer así:

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

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

y luego reproduzco mi video con:

[avPlayer play];

El video se reproduce bien pero se detiene al final. Con MPMoviePlayerController, todo lo que tiene que hacer es establecer su repeatModepropiedad en el valor correcto. No parece haber una propiedad similar en AVPlayer. Tampoco parece haber una devolución de llamada que me diga cuándo ha terminado la película, así que puedo buscar desde el principio y volver a reproducirla.

No estoy usando MPMoviePlayerController porque tiene algunas limitaciones serias. Quiero poder reproducir múltiples transmisiones de video a la vez.

orj
fuente
1
Consulte esta respuesta para obtener un enlace al código de trabajo real: stackoverflow.com/questions/7822808/…
MoDJ

Respuestas:

275

Puedes recibir una Notificación cuando el jugador termine. ChequeAVPlayerItemDidPlayToEndTimeNotification

Al configurar el reproductor:

ObjC

  avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; 

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

Esto evitará que el jugador haga una pausa al final.

en la notificación:

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

Esto rebobinará la película.

No olvide anular el registro de la notificación al liberar al jugador.

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
fuente
66
... y si desea reproducirlo justo después de [p seekToTime: kCMTimeZero] (una especie de "rebobinado"), simplemente haga [p play] nuevamente.
thomax
24
esto no debería ser necesario ... si lo hace avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;no se detendrá, por lo que no hay necesidad de ponerlo a jugar de nuevo
Bastian
Para que el sonido se reproduzca nuevamente, debe llamar [player play];después de rebobinar.
Kenny Winker
12
Esta solución funciona, pero no es completamente transparente. Tengo una pausa muy pequeña. ¿Estoy haciendo algo mal?
Joris van Liempd iDeveloper
3
@Praxiteles, debe anular el registro cuando la vista no se destruye, o cuando elimina el reproductor de video o lo que sea que haga). Puede usar, [[NSNotificationCenter defaultCenter] removeObserver:self];por ejemplo, cuando selfescucha las notificaciones.
Bastian
63

Si ayuda, en iOS / tvOS 10, hay un nuevo AVPlayerLooper () que puede usar para crear un bucle continuo de video (Swift):

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

Esto se presentó en WWDC 2016 en "Avances en la reproducción de AVFoundation": https://developer.apple.com/videos/play/wwdc2016/503/

Incluso usando este código, tuve un problema hasta que presenté un informe de error con Apple y obtuve esta respuesta:

El archivo de película que tiene una duración de película más larga que las pistas de audio / video es el problema. FigPlayer_File está deshabilitando la transición sin espacios porque la edición de la pista de audio es más corta que la duración de la película (15.682 frente a 15.787).

Debe corregir los archivos de película para que la duración de la película y la duración de la pista tengan la misma duración o puede usar el parámetro de rango de tiempo de AVPlayerLooper (establezca el rango de tiempo de 0 a la duración de la pista de audio)

Resulta que Premiere había estado exportando archivos con una pista de audio de una longitud ligeramente diferente al video. En mi caso, estaba bien eliminar el audio por completo, y eso solucionó el problema.

Nabha
fuente
2
Nada más funcionó para mí. Estoy usando un AVPlayerLooper y tuve este error y la solución de la discrepancia entre las longitudes de video / audio resolvió el problema.
Kevin Heap
1
Gracias por esa información sobre Premiere. Agregué un intervalo de tiempo al bucle y eso solucionó mi problema de "video intermitente".
Alexander Flenniken
@Nabha, ¿es posible usar esto durante un período de tiempo determinado dentro del video? Por ejemplo, el video dura 60 segundos, pero solo quiero repetir los primeros 10 segundos
Lance Samaria
29

En Swift :

Puede recibir una notificación cuando el reproductor termine ... verifique AVPlayerItemDidPlayToEndTimeNotification

al configurar el reproductor:

avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None

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

Esto evitará que el jugador haga una pausa al final.

en la notificación:

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)

Esto rebobinará la película.

No olvide anular el registro de la notificación al liberar al jugador.

Rey Mago
fuente
44
Estoy viendo un pequeño inconveniente entre los bucles con este método. Abrí mi video en Adobe Premier y verifiqué que no hay fotogramas duplicados en el video, por lo que el breve inconveniente es definitivamente en la reproducción. ¿Alguien ha encontrado una manera de hacer un bucle de video a la perfección?
SpaceManGalaxy
@SpaceManGalaxy También noté el hipo. ¿Has encontrado una manera de solucionar este problema técnico?
Lance Samaria el
18

Esto es lo que terminé haciendo para evitar el problema de pausa-hipo:

Rápido:

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

C objetivo:

__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: No utilicé avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNoneya que no es necesario.

Islam Q.
fuente
2
@KostiaDombrovsky ¿probaste un dispositivo real o videos diferentes?
Islam Q.
@IslamQ. Grabo un archivo MP4 y luego trato de reproducirlo en un bucle como lo hace Snapchat.
Kostia Dombrovsky
@KostiaDombrovsky ¿comparaste tu reproducción con Snapchat lado a lado? Creo que debido a que los fotogramas iniciales y finales no coinciden, parece que se detuvo, pero nunca se detiene.
Islam Q.
Tampoco funcionó para mí. Tengo un video de 6 segundos con audio incesante y sigo escuchando una fracción de segundo de silencio con este método
Cbas
Estoy viendo una pérdida de memoria cuando uso este enfoque. Tiene que ver con las [weakSelf.avPlayer seekToTime:kCMTimeZero]; [weakSelf.avPlayer play];líneas: cuando comento estas líneas ya no hay una pérdida de memoria. He perfilado esto en instrumentos.
Solsma Dev
3

Recomiendo usar AVQueuePlayer para reproducir sus videos sin problemas. Agregar el observador de notificaciones

AVPlayerItemDidPlayToEndTimeNotification

y en su selector, reproduce tu video

AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];
kevinnguy
fuente
Intenté esto y no muestra ninguna mejora sobre el método sugerido por @Bastian. ¿Lograste eliminar totalmente el hipo con esto?
amadour
2
@amadour lo que puede hacer es agregar 2 de los mismos videos en el reproductor AVQueuePlayer cuando se inicializa y cuando el jugador publica AVPlayerItemDidPlayToEndTimeNotification, agregue el mismo video a la cola del jugador.
kevinnguy
3

Para evitar la brecha cuando se rebobina el video, usar varias copias del mismo activo en una composición funcionó bien para mí. Lo encontré aquí: www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html (enlace ahora muerto).

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];
usuario2581875
fuente
Supongo que te refieres a este enlace devbrief.blogspot.se/2011/12/…
flame3
2

esto funcionó para mí sin problemas de hipo, el punto es detener el reproductor antes de llamar al 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. registro de notificaciones

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

    func videoLoop() {
      self.backgroundPlayer?.pause()
      self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero)
      self.backgroundPlayer?.play()
    }
Vojta
fuente
3
Gracias. Intenté esto, pero todavía hay una pausa para mí.
Nabha
2

Para Swift 3 y 4

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

mi solución en Objective-c con AVQueuePlayer: parece que debe duplicar el AVPlayerItem y al finalizar la reproducción del primer elemento, agregue instantáneamente otra copia. "Tipo de" tiene sentido y funciona para mí sin ningún contratiempo

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

neón M3
fuente
1

Swift 5:

He hecho algunos pequeños ajustes de las respuestas anteriores, como agregar el elemento de jugador a la cola antes de agregarlo a la capa de jugador.

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

Y haga que playerLooper sea una propiedad de su UIViewController, de lo contrario, el video solo puede reproducirse una vez.

NSCoder
fuente
0

Después de cargar el video en AVPlayer (a través de su AVPlayerItem, por supuesto):

 [self addDidPlayToEndTimeNotificationForPlayerItem:item];

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

En su método viewWillDisappear:

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

En la declaración de la interfaz de su controlador de vista dentro del archivo de implementación:

id _notificationToken;

¿Necesita ver esto en funcionamiento antes de intentarlo? Descargue y ejecute esta aplicación de muestra:

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

En mi aplicación, que usa este mismo código, no hay pausa alguna entre el final del video y el comienzo. De hecho, dependiendo del video, no hay forma de que sepa que el video está al principio nuevamente, salvo la visualización del código de tiempo.

James Bush
fuente
0

puede agregar un observador AVPlayerItemDidPlayToEndTimeNotification y reproducir el video desde el inicio en el selector, código como a continuación

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

Lo siguiente me funciona en WKWebView en swift 4.1 La parte principal de WKWebView en 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
fuente
0

Lo que hice es hacer que se reproduzca en bucle, como mi código a continuación:

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

Swift 4.2 en Xcode 10.1.

, hay una forma relativamente fácil de reproducir AVKit/ AVFoundationusar un videoAVQueuePlayer() , la técnica de Observación de valores clave (KVO) y un token para ello.

Esto definitivamente funciona para un montón de videos H.264 / HEVC con una carga mínima para la CPU.

Aquí hay un 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
fuente
-1

use el siguiente código AVPlayerViewController, me funciona

        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 los controles que se mostrarán, espero que sea útil

Iyyappan Ravi
fuente
-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 propiedad ya está definida en su interior AVAudioPlayer. Espero que esto le pueda ayudar. Estoy usando Xcode 6.3.

Julz
fuente
8
eso es para audio, no para AVPlayer
Yariv Nissim