Zapętlanie wideo za pomocą AVFoundation AVPlayer?

142

Czy istnieje stosunkowo łatwy sposób zapętlenia wideo w programie AVFoundation?

Stworzyłem mój AVPlayer i AVPlayerLayer w ten sposób:

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

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

a potem odtwarzam wideo za pomocą:

[avPlayer play];

Film odtwarza się dobrze, ale na końcu się zatrzymuje. Z MPMoviePlayerController wszystko, co musisz zrobić, to ustawić jego repeatModewłaściwość na odpowiednią wartość. Wygląda na to, że w programie AVPlayer nie ma podobnej właściwości. Wygląda na to, że nie ma też wywołania zwrotnego, które powie mi, kiedy film się skończy, więc mogę odszukać początek i odtworzyć go ponownie.

Nie używam MPMoviePlayerController, ponieważ ma on poważne ograniczenia. Chcę mieć możliwość jednoczesnego odtwarzania wielu strumieni wideo.

orj
źródło
1
Zobacz tę odpowiedź, aby uzyskać link do rzeczywistego działającego kodu: stackoverflow.com/questions/7822808/ ...
MoDJ

Odpowiedzi:

275

Możesz otrzymać powiadomienie, gdy gracz się skończy. CzekAVPlayerItemDidPlayToEndTimeNotification

Podczas konfigurowania odtwarzacza:

ObjC

  avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone; 

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

Zapobiegnie to zatrzymaniu gracza na końcu.

w zgłoszeniu:

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

spowoduje to przewinięcie filmu do tyłu.

Nie zapomnij wyrejestrować powiadomienia przy zwalnianiu odtwarzacza.

Szybki

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
źródło
6
... a jeśli chcesz to odtworzyć zaraz po [p seekToTime: kCMTimeZero] (swego rodzaju „przewijanie do tyłu”), po prostu zrób [p play] ponownie.
thomax
24
to nie powinno być konieczne ... jeśli to zrobisz avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;, nie zatrzyma się, więc nie ma potrzeby ustawiania go na ponowne odtwarzanie
Bastian
Aby ponownie odtworzyć dźwięk, musisz zadzwonić [player play];po przewinięciu.
Kenny Winker
12
To rozwiązanie działa, ale nie jest całkowicie płynne. Mam bardzo krótką przerwę. czy robię coś źle?
Joris van Liempd iDeveloper
3
@Praxiteles musisz go wyrejestrować, gdy widok zostanie zniszczony, lub gdy usuniesz odtwarzacz wideo lub cokolwiek robisz,) Możesz użyć [[NSNotificationCenter defaultCenter] removeObserver:self];na przykład, gdy selfsłuchasz powiadomień.
Bastian
63

Jeśli to pomoże, w iOS / tvOS 10 dostępna jest nowa funkcja AVPlayerLooper (), której możesz użyć do tworzenia płynnej pętli wideo (Swift):

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

Zostało to zaprezentowane na WWDC 2016 w „Advances in AVFoundation Playback”: https://developer.apple.com/videos/play/wwdc2016/503/

Nawet używając tego kodu, miałem czkawkę, dopóki nie złożyłem raportu o błędzie w Apple i nie otrzymałem tej odpowiedzi:

Problem stanowi plik filmowy, którego czas trwania filmu jest dłuższy niż ścieżki audio / wideo. FigPlayer_File wyłącza przejście bez przerw, ponieważ edycja ścieżki audio jest krótsza niż czas trwania filmu (15.682 w porównaniu z 15.787).

Musisz albo naprawić pliki filmowe, aby czas trwania filmu i czas trwania ścieżki były takie same lub możesz użyć parametru zakresu czasu AVPlayerLooper (ustaw zakres czasu od 0 do czasu trwania ścieżki dźwiękowej)

Okazuje się, że Premiere eksportowało pliki ze ścieżką dźwiękową o nieco innej długości niż wideo. W moim przypadku całkowite usunięcie dźwięku było w porządku i to rozwiązało problem.

Nabha
źródło
2
Nic innego nie działało dla mnie. Używam AVPlayerLooper i miałem ten błąd, a naprawienie rozbieżności między długością wideo / audio rozwiązało problem.
Kevin Heap
1
Dziękuję za informację o Premiere. Dodałem timeRange do looper i to rozwiązało mój problem z "flashowaniem wideo".
Alexander Flenniken,
@Nabha czy możliwe jest użycie tego przez określony czas w filmie? Na przykład film trwa 60 sekund, ale chcę zapętlić tylko pierwsze 10 sekund
Lance Samaria
29

W Swift :

Możesz otrzymać powiadomienie, gdy odtwarzacz się skończy ... sprawdź AVPlayerItemDidPlayToEndTimeNotification

podczas konfigurowania odtwarzacza:

avPlayer.actionAtItemEnd = AVPlayerActionAtItemEnd.None

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

Zapobiegnie to zatrzymaniu gracza na końcu.

w zgłoszeniu:

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)

spowoduje to przewinięcie filmu do tyłu.

Nie zapomnij wyrejestrować powiadomienia przy zwalnianiu odtwarzacza.

King-Wizard
źródło
4
W przypadku tej metody widzę małą czkawkę między pętlami. Otworzyłem wideo w Adobe Premier i sprawdziłem, czy w filmie nie ma zduplikowanych klatek, więc krótka czkawka jest zdecydowanie podczas odtwarzania. Czy ktoś znalazł sposób na płynne i szybkie tworzenie pętli wideo?
SpaceManGalaxy,
@SpaceManGalaxy Zauważyłem też czkawkę. Czy znalazłeś sposób na naprawienie tej usterki?
Lance Samaria
18

Oto, co ostatecznie zrobiłem, aby zapobiec problemowi z przerwami:

Szybki:

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

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

UWAGA: nie używałem, avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNoneponieważ nie jest potrzebne.

Islam Q.
źródło
2
@KostiaDombrovsky Czy wypróbowałeś na prawdziwym urządzeniu lub w różnych filmach?
Islam Q.
@IslamQ. Nagrywam plik MP4, a następnie próbuję odtworzyć go w pętli, tak jak robi to snapchat.
Kostia Dombrovsky
@KostiaDombrovsky, czy porównałeś swoje odtwarzanie ze snapchatem side-by-side? Myślę, że ponieważ początkowe i końcowe klatki nie pasują, wydaje się, że zostało wstrzymane, ale nigdy się nie zatrzymuje.
Islam Q.
Dla mnie też nie działało. Mam 6-sekundowy film z nieustannym dźwiękiem i ciągle słyszę ułamek sekundy ciszy z tą metodą
Cbas
Widzę wyciek pamięci podczas korzystania z tego podejścia. Ma to związek z [weakSelf.avPlayer seekToTime:kCMTimeZero]; [weakSelf.avPlayer play];liniami - kiedy je komentuję, nie ma już wycieku pamięci. Sprofilowałem to w instrumentach.
Solsma Dev
3

Zalecam używanie AVQueuePlayer do płynnego zapętlania filmów. Dodaj obserwatora powiadomień

AVPlayerItemDidPlayToEndTimeNotification

i w jego selektorze zapętl wideo

AVPlayerItem *video = [[AVPlayerItem alloc] initWithURL:videoURL];
[self.player insertItem:video afterItem:nil];
[self.player play];
kevinnguy
źródło
Próbowałem tego i nie pokazuje to żadnej poprawy w stosunku do metody sugerowanej przez @Bastian. Czy udało ci się całkowicie usunąć czkawkę z tym?
amadour
2
@amadour, co możesz zrobić, to dodać 2 takie same filmy do odtwarzacza AVQueuePlayer po zainicjowaniu, a gdy odtwarzacz opublikuje AVPlayerItemDidPlayToEndTimeNotification, dodaj to samo wideo do kolejki odtwarzacza.
kevinnguy
3

Aby uniknąć przerwy podczas przewijania wideo, dobrze działało użycie wielu kopii tego samego zasobu w kompozycji. Znalazłem to tutaj: www.developers-life.com/avplayer-looping-video-without-hiccupdelays.html (link nie działa ).

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
źródło
Chyba masz na myśli ten link devbrief.blogspot.se/2011/12/…
flame3
2

to działało dla mnie bez problemów z czkawką, chodzi o wstrzymanie odtwarzacza przed wywołaniem metody 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. zgłoszenie rejestracji

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

    func videoLoop() {
      self.backgroundPlayer?.pause()
      self.backgroundPlayer?.currentItem?.seekToTime(kCMTimeZero)
      self.backgroundPlayer?.play()
    }
Vojta
źródło
3
Dzięki - próbowałem tego, ale wciąż jest dla mnie przerwa.
Nabha,
2

Dla Swift 3 i 4

NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.avPlayer?.currentItem, queue: .main) { _ in
     self.avPlayer?.seek(to: kCMTimeZero)
     self.avPlayer?.play()
}
vp2698
źródło
1

moje rozwiązanie w objective-c z AVQueuePlayer - wydaje się, że musisz zduplikować AVPlayerItem i po zakończeniu odtwarzania pierwszego elementu natychmiast dodać kolejną kopię. „Kind of” ma sens i działa dla mnie bez żadnych problemów

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
źródło
1

Swift 5:

Dokonałem niewielkich zmian w stosunku do poprzednich odpowiedzi, takich jak dodanie elementu playerItem do kolejki przed dodaniem go do warstwy 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()

I ustaw playerLooper jako właściwość Twojego UIViewController, w przeciwnym razie wideo może zostać odtworzone tylko raz.

NSCoder
źródło
0

Po załadowaniu wideo do AVPlayera (oczywiście przez jego AVPlayerItem):

 [self addDidPlayToEndTimeNotificationForPlayerItem:item];

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

W Twoim widoku metoda WillDisappear:

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

W Twoim widoku deklaracja interfejsu kontrolera w pliku implementacji:

id _notificationToken;

Chcesz zobaczyć, jak działa, zanim spróbujesz? Pobierz i uruchom tę przykładową aplikację:

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

W mojej aplikacji, która używa tego właśnie kodu, nie ma żadnej przerwy między końcem filmu a początkiem. W rzeczywistości, w zależności od wideo, nie mogę stwierdzić, że wideo jest ponownie na początku, zapisz wyświetlanie kodu czasowego.

James Bush
źródło
0

możesz dodać obserwatora AVPlayerItemDidPlayToEndTimeNotification i odtworzyć wideo od początku w selektorze, kod jak poniżej

 //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
źródło
0

Poniższe działa dla mnie w WKWebView w Swift 4.1 Główna część WKWebView w 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
źródło
0

Zrobiłem to, aby odtwarzać w pętli, jak mój kod poniżej:

[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
źródło
0

Swift 4.2 w Xcode 10.1.

Tak , istnieje stosunkowo łatwy sposób na zapętlenie wideo w AVKit/ AVFoundationza pomocą AVQueuePlayer()techniki obserwacji klucz-wartość (KVO) i jej tokena.

To zdecydowanie działa w przypadku kilku filmów H.264 / HEVC przy minimalnym obciążeniu procesora.

Oto kod:

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
źródło
-1

użyj poniższego kodu AVPlayerViewController, działa dla mnie

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

Wszystkie kontrolki do pokazania, mam nadzieję, że będą pomocne

Iyyappan Ravi
źródło
-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;

Ta właściwość jest już zdefiniowana w środku AVAudioPlayer. Mam nadzieję, że to może ci pomóc. Używam Xcode 6.3.

Julz
źródło
8
to dla audio, nie dla AVPlayera
Yariv Nissim