AVPlayer “freezes” the app at the start of buffering an audio stream

梦想与她 提交于 2019-11-28 15:47:52
Gigisommo

Don't use playerItemWithURL it's sync. When you receive the response with the url try this:

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

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

Bump, since this is a highly rated question and similar questions online either has outdated answers or aren't great. The whole idea is pretty straight forward with AVKit and AVFoundation, which means no more depending on third party libraries. The only issue is that it took some tinkering around and put the pieces together.

AVFoundation's Player() initialization with url is apparently not thread safe, or rather it's not meant to be. Which means, no matter how you initialize it in a background thread the player attributes are going to be loaded in the main queue causing freezes in the UI especially in UITableViews and UICollectionViews. To solve this issue Apple provided AVAsset which takes a URL and assists in loading the media attributes like track, playback, duration etc. and can do so asynchronously, with a best part being that this loading process is cancellable (unlike other Dispatch queue background threads where ending a task may not be that straight forward). This means, there is no need to worry about lingering zombie threads in the background as you scroll fast on a table view or collection view, ultimately piling up on the memory with a whole bunch of unused objects. This cancellable feature is great, and allows us to cancel any lingering AVAsset async load if it is in progress but only during cell dequeue. The async loading process can be invoked by the loadValuesAsynchronously method, and can be cancelled (at will) at any later time (if still in progress).

Don't forget to exception handle properly using the results of loadValuesAsynchronously. In Swift (3/4), here's how you would would load a video asynchronously and handle situations if the async process fails (due to slow networks, etc.)-

TL;DR

TO PLAY A VIDEO

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

NOTE:

Based on what your app wants to achieve you may still have to do some amount of tinkering to tune it to get smoother scroll in a UITableView or UICollectionView. You may also need to implement some amount of KVO on the AVPlayerItem properties for it to work and there's plenty of posts here in SO that discuss AVPlayerItem KVOs in detail.


TO LOOP THROUGH ASSETS (video loops/GIFs)

To loop a video, you can use the same method above and introducing AVPlayerLooper. Here's a sample code to loop a video (or perhaps a short video in GIF style). Note the use of duration key which is required for our video loop.

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

EDIT : As per the documentation, AVPlayerLooper requires the duration property of the asset to be fully loaded in order to be able to loop through videos. Also, the timeRange: timeRange with the start and end timerange in the AVPlayerLooper initialization is really optional if you want an infinite loop. I have also realized since I posted this answer that AVPlayerLooper is only about 70-80% accurate in looping videos, especially if your AVAsset needs to stream the video from a URL. In order to solve this issue there is a totally different (yet simple) approach to loop a video-

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

Gigisommo's answer for Swift 3 including the feedback from the comments:

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)

        }

}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!