AVPlayerにおける再生終端の検知とループの実装
#AVPlayer
やりたいこと
AVPlayer にて、再生が終端に到達したことを適切に検知したい
AVPlayer にて、ループ再生を実装したい
TL;DR
終端の検知には AVPlayerItemDidPlayToEndTime (iOS 4.0+) を利用できる
ただし、AVPlayer.actionAtItemEnd (iOS 4.0+) の設定によっては検知できないケースがあるので注意する
終端到達時の状態が誤っていると、再生が終了してもバックグラウンドでアプリが Suspend されずにアプリが生き続け、バッテリー寿命を下げる結果になってしまうので注意する
シームレスなループの実装には AVQueuePlayer, AVPlayerLooper を利用できる
ただし、動画データと音声データの長さはちゃんと揃えておくこと
終端の検知について
ある AVPlayerItem の再生が終了したかどうか?の検知に利用できる API は 2 つある。
AVPlayerItemDidPlayToEndTime (iOS 4.0+)
AVPlayerItem が終端まで再生された際に通知される Notification
AVPlayer.actionAtItemEnd (iOS 4.0+)
AVPlayerItem が再生を終了した際の挙動を設定することができる
advance/pause/none のいずれかを設定でき、advance の場合は存在すれば次の Item が再生され、pause であれば再生が一時停止する
none の場合、終端に到達して映像が止まっている状態は、内部的には pause 状態にはなっていない ようなので注意
例えば、終端に到達して映像は停止しても、timeControlStatus は .paused にはならず、.playing のままになる
さらに、この状態だと 映像が停止しているとみなされないため、バックグラウンドでアプリが生き続け、バッテリー寿命を減らす原因となる可能性がある ので注意
actionAtItemEnd が none の場合に終端まで再生し切った際の AVPlayerItem の状態が内部でどうなっているのか?については、イマイチ解説されている公式の資料は見つけられなかった。ただ、試してみた感じだと、この終端検知については OS 毎に若干の挙動差があったため、よく検証した上で利用した方が良さそうだった。以下に、発生を確認できた問題を列挙しておく。
AVPlayerItemDidPlayToEndTime が通知されないパターン
HLS 再生にて、AVPlayerItemDidPlayToEndTime が通知されたら先頭までシークすることでループさせた
actionAtItemEnd が none の場合だと、2巡目以降で AVPlayerItemDidPlayToEndTime が受け取れなかった
actionAtItemEnd が pause でも、終端到達時に停止判定にならず、バックグラウンドでアプリが生き続けてしまう
iOS 11 で再現せず、iOS 12 で再現するパターンがあった
一応、actionAtItemEnd に none を設定した上で、AVPlayerItemDidPlayToEndTime 検知時に明示的に AVPlayer.pause() すると、先頭までシークする形式のループでも、二巡目以降も問題なく終端検知が行えたが、これが良い方法なのかはわからない。
ループについて
古典的なループの実装方法
ループ再生を実装したい場合、終端検知の項目でも記載した通り、典型的な手法としては、AVPlayerItem が終端まで再生されたことを AVPlayerItemDidPlayToEndTime で検知して、シークで先頭まで戻すような実装が考えられる。
が、この方法には以下のような問題がある。
終端まで到達した通知を受け取ってからプレーヤをシーク操作するまでの間に時間がかかるため、レイテンシーが生じる
先頭のメディアデータの再生準備 (WWDC Video では preroll と呼ばれる) が瞬時には行えないので、この再生準備によってもレイテンシーが生じる
https://developer.apple.com/videos/play/wwdc2016-503/?time=796
最適なループの実装方法
以下の方針で実装すると、このレイテンシーを小さくしつつループを実装できる。
再生対象のコンテンツとなる同一の AVAsset に対し、複数の AVPlayerItem を用意する
複数の AVPlayerItem を AVQueuePlayer にエンキューし、Item の連続再生としてループ再生を実現する
AVQueuePlayer は、近い将来再生するいくつかの Item 群を設定&シームレスに再生するためのプレーヤ
これにより、シームレスなループを実現することができる
ループの度に、古い AVPlayerItem の削除 & 新しい AVPlayerItem の追加を行う (WWDC Video では Treadmill と呼ばれる作業)
上記の Treadmill を自動で行ってくれる AVPlayerLooper を利用する
AVQueuePlayer で Treadmill を手動で実装することもできる。サンプルコード の QueuePlayerLooper がその実装になる。下記のような方針。
1. 同一 Asset に対して複数の AVPlayerItem を生成し、AVQueuePlayer を初期化する
2. AVQueuePlayer.currentItem を Observe する
3. currentItem が切り替わったら、古い currentItem を先頭までシークした上で、AVQueuePlayer の末尾に追加する
この間、KVO は一時停止しておく
上記のパターンを手動で実装するのは面倒という要望から、AVPlayerLooper というクラスが誕生した。サンプルコード の PlayerLooper がその実装になる。下記のようにシンプルに記述できる。
code:swift
let player = AVQueuePlayer()
let playerLayer = AVPlayerLayer(player: player)
let playerItem = AVPlayerItem(url: videoUrl)
let playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
player.play()
注意点として、音声と動画の長さが同一である必要がある という点が挙げられる。同一でない場合、AVPlayer がどんな振る舞いをすれば良いのか決定できないため、だそう。
参考
Advances in AVFoundation Playback - WWDC 2016