AVPlayer «замораживает» приложение в начале буферизации аудиопотока

#ios #audio-streaming #dropbox #avplayer #avqueueplayer

#iOS #потоковое аудио #dropbox #avplayer #avqueueplayer

Вопрос:

Я использую подкласс AVQueuePlayer , и когда я добавляю новый AVPlayerItem с потоковым URL, приложение зависает примерно на секунду или две. Под замораживанием я подразумеваю, что оно не реагирует на прикосновения к пользовательскому интерфейсу. Кроме того, если у меня уже есть песня, а затем я добавляю еще одну в очередь, AVQueuePlayer автоматически запускается предварительная загрузка песни, пока она все еще транслируется первой. Это заставляет приложение не реагировать на прикосновения к пользовательскому интерфейсу в течение двух секунд, как при добавлении первой песни, но песня все еще воспроизводится. Таким образом, это означает AVQueuePlayer , что в основном потоке что-то делается, что вызывает кажущееся «замораживание».

Я использую insertItem:afterItem: , чтобы добавить свой AVPlayerItem . Я протестировал и убедился, что именно этот метод вызывал задержку. Возможно, это может быть что-то, что AVPlayerItem происходит, когда оно активируется AVQueuePlayer в момент добавления его в очередь.

Должен указать, что я использую бета-версию Dropbox API v1 для получения URL-адреса потоковой передачи с помощью этого вызова метода:

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

Затем, когда я получаю URL-адрес потока, я отправляю его AVQueuePlayer следующим образом:

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

Итак, мой вопрос: как мне избежать этого?
Должен ли я выполнять предварительную загрузку аудиопотока самостоятельно, без помощи AVPlayer ? Если да, то как мне это сделать?

Спасибо.

Комментарии:

1. Произойдет ли это, даже если оно вызывается в другом потоке? Вы пробовали отсоединить и вызвать его там?

2. Я пытался, но не сработало. Затем позже я прочитал документацию и выяснил, что методы AVPlayer всегда должны вызываться в основном потоке.

3. Вы можете попробовать зависнуть на AVPlayerItem, пока его статус не станет AVPlayerItemStatusReadyToPlay. Например, проверяйте несколько раз в секунду, готово ли оно к воспроизведению, а затем, когда оно готово, добавьте его в очередь. Возможно, insertItem синхронно ожидает готовности AVPlayerItem.

4. Это все еще не означает, что оно не будет зависать между песнями. Я не хочу, чтобы оно когда-либо зависало.

5. у меня та же проблема. Вместо этого я должен использовать AVPlayer.

Ответ №1:

Не используйте playerItemWithURL его синхронизацию. Когда вы получите ответ с URL-адресом, попробуйте это:

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

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

Комментарии:

1. Очень важно отметить, что блок completionHandler не вызывается в основном потоке (или, по крайней мере, не гарантируется), тогда вы должны использовать dispatch_async(dispatch_get_main_queue(), ^{ }) внутри него.

2. Как я могу добиться этого в AVPlayer. На самом деле, я не использую AVQUEUEPLAYER. Можете ли вы дать мне какую-нибудь идею для достижения 🙂 @Gigisommo

3. Если вы просто используете AVPlayer, вы можете следовать тому же подходу, но вместо «insertItem» используйте «replaceCurrentItemWithPlayerItem». (И чтобы получить актив из AVPlayerItem, используйте «item.asset».)

4. ты бог среди людей.

5. Каковы все возможные ключи?

Ответ №2:

Удар, поскольку это вопрос с высоким рейтингом, а подобные вопросы в Интернете либо имеют устаревшие ответы, либо не очень хороши. Вся идея довольно проста в AVKit использовании и AVFoundation , что означает, что больше не зависит от сторонних библиотек. Единственная проблема заключается в том, что потребовалось немного повозиться и собрать кусочки воедино.

AVFoundation Player() инициализация с помощью, по- url видимому, не является потокобезопасной, или, скорее, так не должно быть. Это означает, что независимо от того, как вы инициализируете его в фоновом потоке, атрибуты проигрывателя будут загружены в основную очередь, вызывая зависания в пользовательском интерфейсе, особенно в UITableView s и UICollectionViews . Чтобы решить эту проблему, Apple предоставила AVAsset , которая принимает URL-адрес и помогает загружать атрибуты мультимедиа, такие как дорожка, воспроизведение, длительность и т. Д., И может делать это асинхронно, и самое приятное, что этот процесс загрузки можно отменить (в отличие от других фоновых потоков очереди отправки, где завершение задачи может быть не таким простым). Это означает, что нет необходимости беспокоиться о затяжных потоках зомби в фоновом режиме при быстрой прокрутке табличного или коллекционного представления, что в конечном итоге приводит к накоплению в памяти целой кучи неиспользуемых объектов. Эта cancellable функция великолепна и позволяет нам отменить любую длительную AVAsset асинхронную загрузку, если она выполняется, но только во время удаления ячейки из очереди. Процесс асинхронной загрузки может быть вызван loadValuesAsynchronously методом и может быть отменен (по желанию) в любое более позднее время (если все еще продолжается).

Не забудьте правильно обработать исключение, используя результаты loadValuesAsynchronously . В Swift (3/4) вот как вы бы загружали видео асинхронно и обрабатывали ситуации, если асинхронный процесс завершается неудачно (из-за медленных сетей и т. Д.)-

TL; DR

ДЛЯ ВОСПРОИЗВЕДЕНИЯ ВИДЕО

 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: amp;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
   }
})
  

ПРИМЕЧАНИЕ:

В зависимости от того, чего хочет достичь ваше приложение, вам, возможно, все равно придется немного поработать, чтобы настроить его для более плавной прокрутки в UITableView или UICollectionView . Вам также может потребоваться реализовать некоторое количество KVO в AVPlayerItem свойствах, чтобы оно работало, и здесь есть много сообщений в SO AVPlayerItem , в которых подробно обсуждаются KVO.


ДЛЯ ПЕРЕБОРА РЕСУРСОВ (циклы видео / GIF)

Чтобы зациклить видео, вы можете использовать тот же метод, описанный выше и представленный AVPlayerLooper . Вот пример кода для циклического воспроизведения видео (или, возможно, короткого видео в стиле GIF). Обратите внимание на использование duration ключа, который требуется для нашего видео цикла.

 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: amp;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
   }
})
  

РЕДАКТИРОВАТЬ: согласно документации, AVPlayerLooper требуется duration , чтобы свойство ресурса было полностью загружено, чтобы можно было просматривать видео. Кроме того, timeRange: timeRange с диапазоном времени начала и окончания в AVPlayerLooper инициализации действительно необязательно, если вы хотите бесконечный цикл. С тех пор, как я опубликовал этот ответ, я также понял AVPlayerLooper , что точность при циклическом воспроизведении видео составляет всего около 70-80%, особенно если вам AVAsset необходимо транслировать видео с URL. Для решения этой проблемы существует совершенно другой (но простой) подход к циклическому воспроизведению видео-

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

Комментарии:

1. Я перевел код на Objective-c, и он все еще не работает. Код замораживает основной поток.

Ответ №3:

Ответ Gigisommo для Swift 3, включая отзывы из комментариев:

 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)

        }

}