AVPlayerの実装におけるベストプラクティス
概要
実装のベストプラクティスをまとめていきます
スレッドのブロックを避ける
TODO
Stall を避ける
TODO
再生開始までの起動時間を早くする
ローカルファイルにおける最適化
ローカルファイルの URL から動画を再生する場合を考えてみる。下記のようなシンプルなコードになるはずで、これは正しい。
code:swift
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
let playerLayer = AVPlayerLayer(player: player)
が、AVFoundation の立場に立つと少し効率の悪い部分がある。AVPlayer は playerItem を受け取るとすぐに再生用のパイプラインを設定するが、playerItem を受け取った時点では 将来 playerLayer にアタッチされて映像も描画することになる ことを AVPlayer は知らない。そのため、AVPlayer(playerItem: playerItem) が初期化された時点では、内部的には音声のみの生成用のパイプラインが設定される。その後 AVPlayerLayer にアタッチされて初めて映像が必要なことがわかり、この時点で AVPlayer 内部で再設定が行われる。 AVFoundation に対しては、必要な時に必要な情報を渡すような方式にしたほうが無駄な設定が行われなくて効率が良い。そのため、以下のように記述するのが良い。 code:swift
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer()
let playerLayer = AVPlayerLayer(player: player)
player.replaceCurrentItemWithPlayerItem(playerItem)
さらに一般化してみると、以下のような感じになる。
1. AVPlayer と AVPlayerItem オブジェクトを初期化する
2. AVPlayer を AVPlayerLayer に、AVPlayerItem を AVPlayerItemVideoOutput になど、プロパティの設定を行う
3. AVPlayer.play() を呼び出す
4. player.replaceCurrentItemWithPlayerItem(playerItem) でアイテムを設定する
手順 4 の前に 3 を行うのが奇妙に見えるが、アイテムを設定する前に再生を指示することができる。例えば、3 と 4 を逆にした場合、AVPlayer は映像を描画することはわかっているけれど、静止画を描画するのか動画を描画するのかはわかっていない。先に AVPlayer.play() を実行しておくことで、AVPlayer に動画を再生するということを伝えておき、それから AVPlayerItem を設定することで、余分な設定が行われることを省き、数 msec が節約できるかもしれない。
HLS において、再生開始前に必要な情報を事前ロードする HLS は再生開始までにいくつかの通信が必要になる。このラウンドトリップのレイテンシーを低減したい。 暗号化されている場合は、content key の取得
これらのいくつかを実際に映像が再生開始される前にできないか考える。例えば、動画のタイトルのみを表示するなどしてまだ再生は行わないでおき、その間にユーザにはコンテンツを再生するかどうか検討してもらう。再生ボタンが押されるまでの間に、少量のネットワーク I/O を済ませておくことはできないか。
code:swift
var asset = AVURLAsset(url: url)
asset.loadValuesAsynchronously(forKeys: "duration", completionHandler: nil) code:swift
asset.preloadsEligibleContentKeys = true
iOS 10.0+ では、これに加えて media segment の取得を事前に行える API として新たに preferredForwardBufferDuration という API が追加された。これは、フォワードバッファの秒数を明示的に指定する API で、低い値にするとすぐに stall に陥る可能性が高くなり、高い値にするとシステムリソースの要求が高くなる。飽くまで再生開始前の preload 用に指定しておくのが推奨の利用方法なので、再生開始次第 0 に設定することで、元のバッファリングアルゴリズムを利用するように戻すべき。 code:swift
var playerItem = AVPlayerItem(asset: asset)
playerItem.preferredForwardBufferDuration = CMTime(value: 5 timescale: 1)
let player = AVPlayer()
let playerLayer = AVPlayerLayer(player: player)
player.replaceCurrentItemWithPlayerItem(playerItem)
Retina ディスプレイの iOS デバイスの場合、contentScale を手動で設定する必要がある (2016年時点)
ネットワークの状態
So what this means is that AVFoundation needs to take into account both the display dimensions and the network bitrate when choosing the variant.
code:swift
if let lastAccessLogEvent = previousPlayerItem.accessLog()?.events.last {
lastObservedBitrate = lastAccessLogEvent.observedBitrate
}
2 つ目は、アプリ側で master playlist を書き換えてしまう方法。このためには、URL にまずカスタム URL スキームを設定しておき、resouce loader の delegate 内でそのカスタム URL スキームのリクエストを受け取ったら、書き換えた master playlist を返してやる必要がある。 code:swift
var asset = AVURLAsset(url: NSURL(string: "myscheme://file.m3u8"))
asset.resourceLoader.setDelegate(...)