Pytanie AVPlayer "zawiesza" aplikację na początku buforowania strumienia audio


Używam podklasy AVQueuePlayer i kiedy dodaję nowe AVPlayerItem w przypadku URL-a strumieniowego aplikacja zatrzymuje się na około sekundę lub dwie. Przez zamrażanie mam na myśli to, że nie reaguje na dotyk w interfejsie użytkownika. Jeśli mam już utwór odtwarzany, a następnie dodaję kolejną do kolejki, AVQueuePlayer automatycznie rozpoczyna wstępne ładowanie utworu, gdy nadal przesyła strumieniowo pierwszą. To sprawia, że ​​aplikacja nie reaguje na dotknięcia w interfejsie przez dwie sekundy, tak jak podczas dodawania pierwszego utworu, ale utwór wciąż jest odtwarzany. Wiec to znaczy AVQueuePlayer robi coś w głównym wątku, który powoduje pozorne "zamrożenie".

ja używam insertItem:afterItem: aby dodać moje AVPlayerItem. Przetestowałem i upewniłem się, że jest to metoda powodująca opóźnienie. Może to może być coś takiego AVPlayerItem robi się, gdy zostanie aktywowany przez AVQueuePlayer w momencie dodania go do kolejki.

Zwróć uwagę, że korzystam z wersji beta Dropbox API w wersji beta, aby uzyskać adres URL przesyłania strumieniowego za pomocą tej metody:

[[self restClient] loadStreamableURLForFile:metadata.path];

Następnie, gdy otrzymam adres URL strumienia, wyślę go do AVQueuePlayer następująco:

[self.player insertItem:[AVPlayerItem playerItemWithURL:url] afterItem:nil];

Moje pytanie brzmi: jak tego uniknąć? Czy powinienem sam przeprowadzić wstępne ładowanie strumienia audio bez pomocy AVPlayer? Jeśli tak, jak to zrobić?

Dzięki.


37
2017-10-09 03:14


pochodzenie


Czy tak się stanie, nawet jeśli zostanie wywołany w innym wątku? Czy próbowałeś go odłączyć i wywołać? - Stavash
Próbowałem, ale nie działałem. Później czytam dokumentację i okazuje się, że metody AVPlayer powinny być zawsze wywoływane w głównym wątku. - raixer
Możesz spróbować powiesić na AVPlayerItem, dopóki jego status nie będzie AVPlayerItemStatusReadyToPlay. Na przykład sprawdzaj kilka razy na sekundę, aby sprawdzić, czy jest gotowy do gry, a następnie, gdy jest dodawany do kolejki. Być może insertItem czeka synchronicznie, aby AVPlayerItem był gotowy. - Fls'Zen
To nadal nie oznacza, że ​​nie zamrozi się między piosenkami. Nie chcę, żeby kiedykolwiek zamarzł. - raixer
ten sam problem dla mnie. Zamiast tego muszę użyć AVPlayer. - xhan


Odpowiedzi:


Nie używaj playerItemWithURL jest zsynchronizowany. Po otrzymaniu odpowiedzi za pomocą adresu URL spróbuj tego:

AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = @[@"playable"];

[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
    [self.player insertItem:[AVPlayerItem playerItemWithAsset:asset] afterItem:nil];
}];

69
2017-08-03 10:28



Bardzo ważne jest, aby zauważyć, że blok zakończenia obsługi nie jest wywoływany w głównym wątku (lub przynajmniej nie jest gwarantowany), wtedy powinieneś użyć wewnątrz niego dispatch_async (dispatch_get_main_queue (), ^ {}). - xissburg
Dodałem @ "tracks" i @ "duration" do tablicy "keys", aby rozwiązać podobny problem w moim przypadku. - optimus
Jak mogę to osiągnąć w AVPlayer. Właściwie to nie używam AVQUEUEPLAYER. Czy możesz dać mi jakiś pomysł na osiągnięcie :) @Gigisommo - iTag
Jeśli używasz AVPlayera, możesz zastosować to samo podejście, ale zamiast "insertItem" użyj "replaceCurrentItemWithPlayerItem". (Aby pobrać zasób z AVPlayerItem użyj "item.asset".) - DennisWelu
jesteś bogiem między ludźmi. - sosale151


Odpowiedź Gigisommo dla Swift 3 w tym opinie z komentarzy:

let asset = AVAsset(url: url)
let keys: [String] = ["playable"]

asset.loadValuesAsynchronously(forKeys: keys) { 

        DispatchQueue.main.async {

            let item = AVPlayerItem(asset: asset)
            self.playerCtrl.player = AVPlayer(playerItem: item)

        }

}

3
2018-05-25 14:32



Hej, mam podobny problem, proszę, sprawdź stackoverflow.com/questions/45555241/... - skyguy


Bump, ponieważ jest to wysoko oceniane pytanie i podobne pytania w Internecie albo ma nieaktualne odpowiedzi, albo nie są świetne. Cały pomysł jest całkiem prosty AVKit i AVFoundation, co oznacza brak w zależności od bibliotek zewnętrznych. Jedyny problem polega na tym, że trzeba było trochę majsterkować i złożyć kawałki razem.

AVFoundation„s Player() inicjalizacja za pomocą url najwyraźniej nie jest bezpieczna dla wątków, a raczej nie jest przeznaczona. Oznacza to, że niezależnie od sposobu zainicjowania go w wątku w tle, atrybuty gracza zostaną załadowane do głównej kolejki powodując zawieszenie w interfejsie użytkownika, szczególnie w UITableViews i UICollectionViews. Aby rozwiązać ten problem, firma Apple dostarczyła AVAsset która pobiera adres URL i pomaga ładować atrybuty multimediów, takie jak ścieżka, odtwarzanie, czas trwania itd., i może to robić asynchronicznie, z najlepszą częścią tego procesu ładowania (można to zrobić inaczej niż inne wątki tła w kolejce Dispatch, gdzie zakończenie zadania może nie być możliwe) to prosto). Oznacza to, że nie musisz się martwić o wątki zombie w tle podczas przewijania w widoku tabeli lub widoku kolekcji, ostatecznie układając w pamięci całą masę nieużywanych obiektów. To cancellable funkcja jest świetna i pozwala nam anulować wszelkie opóźnienia AVAsset ładowanie asynchroniczne, jeśli jest w toku, ale tylko podczas usuwania sekwencji. Proces ładowania asynchronicznego może zostać wywołany przez loadValuesAsynchronously Metoda ta może być anulowana (dowolnie) w dowolnym późniejszym czasie (jeśli nadal trwa).

Nie zapomnij o wyjątku obsługiwać poprawnie używając wyników loadValuesAsynchronously. W Swift (3/4), oto jak byś ładował wideo asynchronicznie i radził sobie z sytuacjami, jeśli proces asynchronizacji się nie powiedzie (z powodu powolnych sieci itp.) -

TL; DR

ODTWARZANIE WIDEO

let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable"]
var player: AVPlayer!                

asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
     var error: NSError? = nil
     let status = asset.statusOfValue(forKey: "playable", error: &error)
     switch status {
        case .loaded:
             DispatchQueue.main.async {
               let item = AVPlayerItem(asset: asset)
               self.player = AVPlayer(playerItem: item)
               let playerLayer = AVPlayerLayer(player: self.player)
               playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
               playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
               self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
               self.player.isMuted = true
               self.player.play()
            }
            break
        case .failed:
             DispatchQueue.main.async {
                 //do something, show alert, put a placeholder image etc.
            }
            break
         case .cancelled:
            DispatchQueue.main.async {
                //do something, show alert, put a placeholder image etc.
            }
            break
         default:
            break
   }
})

UWAGA:

W zależności od tego, co chce osiągnąć Twoja aplikacja, możesz nadal potrzebować odrobiny majsterkowania, aby uzyskać płynniejsze przewijanie UITableView lub UICollectionView. Może być również konieczne wdrożenie pewnej ilości KVO na AVPlayerItem właściwości do działania i jest mnóstwo postów w SO, które dyskutują AVPlayerItem KVO w szczegółach.


PĘTLAĆ PRZEZ AKTYWA (pętle wideo / GIF)

Aby zapętlić wideo, możesz użyć tej samej metody powyżej i wprowadzić AVPlayerLooper. Oto przykładowy kod do zapętlenia wideo (lub może krótki film w stylu GIF). Uwaga sposób użycia duration klucz wymagany dla naszej pętli wideo.

let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable","duration"]
var player: AVPlayer!
var playerLooper: AVPlayerLooper!

asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
     var error: NSError? = nil
     let status = asset.statusOfValue(forKey: "duration", error: &error)
     switch status {
        case .loaded:
             DispatchQueue.main.async {
                 let playerItem = AVPlayerItem(asset: asset)
                 self.player = AVQueuePlayer()
                 let playerLayer = AVPlayerLayer(player: self.player)

                 //define Timerange for the loop using asset.duration
                 let duration = playerItem.asset.duration
                 let start = CMTime(seconds: duration.seconds * 0, preferredTimescale: duration.timescale)
                 let end = CMTime(seconds: duration.seconds * 1, preferredTimescale: duration.timescale)
                 let timeRange = CMTimeRange(start: start, end: end)

                 self.playerLooper = AVPlayerLooper(player: self.player as! AVQueuePlayer, templateItem: playerItem, timeRange: timeRange)
                 playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
                 playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
               self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
                 self.player.isMuted = true
                 self.player.play()
            }
            break
        case .failed:
             DispatchQueue.main.async {
                 //do something, show alert, put a placeholder image etc.
            }
            break
         case .cancelled:
            DispatchQueue.main.async {
                //do something, show alert, put a placeholder image etc.
            }
            break
         default:
            break
   }
})

EDYTOWAĆ : Zgodnie z dokumentacja, AVPlayerLooper wymaga duration właściwość zasobu, który ma być w pełni załadowany, aby móc przeglądać wideo. Ponadto timeRange: timeRange z licznikiem czasu początkowego i końcowego w AVPlayerLooper inicjalizacja jest naprawdę opcjonalna, jeśli chcesz nieskończoną pętlę. Uświadomiłem sobie również, że opublikowałem tę odpowiedź AVPlayerLooper to tylko około 70-80% dokładności w zapętlaniu wideo, szczególnie jeśli twoje AVAsset musi przesyłać strumieniowo wideo z adresu URL. W celu rozwiązania tego problemu istnieje zupełnie inne (ale proste) podejście do pętli wideo

//this will loop the video since this is a Gif
  let interval = CMTime(value: 1, timescale: 2)
  self.timeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
                            if let totalDuration = self.player?.currentItem?.duration{
                                if progressTime == totalDuration{
                                    self.player?.seek(to: kCMTimeZero)
                                    self.player?.play()
                                }
                            }
                        })

3
2018-01-20 00:40