web-dev-qa-db-ja.com

AVPlayerは、オーディオストリームのバッファリングの開始時にアプリを「フリーズ」します

私はAVQueuePlayerのサブクラスを使用しており、ストリーミングURLで新しいAVPlayerItemを追加すると、アプリが約1〜2秒フリーズします。フリーズとは、UIへのタッチに反応しないことを意味します。また、すでに曲を再生していて、別の曲をキューに追加した場合、AVQueuePlayerは最初の曲のストリーミング中に自動的に曲のプリロードを開始します。これにより、最初の曲を追加するときと同様に、UIへのタッチにアプリが2秒間応答しなくなりますが、曲はまだ再生されています。つまり、これは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の助けを借りずに、自分でオーディオストリームのプリロードを行う必要がありますか?もしそうなら、どうすればいいですか?

ありがとう。

48
raixer

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];
}];
69
Gigisommo

バンプ、これは非常に評価された質問であり、オンラインでの同様の質問は回答が古くなっているか、あまりよくありません。 AVKitAVFoundationを使用すると、全体のアイデアは非常に簡単です。つまり、サードパーティのライブラリに依存する必要がなくなります。唯一の問題は、いじくり回して部品を組み合わせるということです。

AVFoundationPlayer()を使用したurlによる初期化は、スレッドセーフではないようです。つまり、スレッドセーフではありません。つまり、バックグラウンドスレッドでどのように初期化しても、プレーヤーの属性がメインキューに読み込まれ、特にUITableViewsとUICollectionViewsでUIがフリーズします。この問題を解決するにはApple AVAssetが提供されます。これは、URLを受け取り、トラック、再生、再生時間などのメディア属性の読み込みを支援します。この読み込みプロセスはキャンセル可能であること(タスクを終了することが他のDispatchキューのバックグラウンドスレッドとは異なります)。これは、テーブルビューで高速にスクロールするときに、バックグラウンドでゾンビスレッドが残っていることを心配する必要がないことを意味します。またはコレクションビュー。最終的には、未使用のオブジェクトがたくさんあるメモリに蓄積されます。この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: &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でスムーズにスクロールするために、アプリを調整するために多少の調整が必要になる場合があります。それが機能するためには、AVPlayerItemプロパティにある程度のKVOを実装する必要がある場合もあります。ここの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: &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 [〜#〜]documentation に従って、AVPlayerLooperには動画をループできるようにするために完全に読み込まれるアセットのdurationプロパティ。また、timeRange: timeRangeは、無限ループが必要な場合、AVPlayerLooper初期化の開始時間範囲と終了時間範囲を使用して、オプションです。この回答を投稿してから、AVPlayerLooperは特にAVAssetがURLから動画をストリーミングする必要がある場合に、動画のループで約70〜80%だけ正確であることにも気付きました。この問題を解決するために、ビデオをループするためのまったく異なる(まだ単純な)アプローチがあります。

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

Swiftに対するGigisommoの回答には、コメントからのフィードバックが含まれています。

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)

        }

}
5
David Seek