AVPlayerにおける再生終端の検知とループの実装
やりたいこと
TL;DR
終端到達時の状態が誤っていると、再生が終了してもバックグラウンドでアプリが Suspend されずにアプリが生き続け、バッテリー寿命を下げる結果になってしまうので注意する
ただし、動画データと音声データの長さはちゃんと揃えておくこと
終端の検知について
advance/pause/none のいずれかを設定でき、advance の場合は存在すれば次の Item が再生され、pause であれば再生が一時停止する
none の場合、終端に到達して映像が止まっている状態は、内部的には pause 状態にはなっていない ようなので注意
さらに、この状態だと 映像が停止しているとみなされないため、バックグラウンドでアプリが生き続け、バッテリー寿命を減らす原因となる可能性がある ので注意
actionAtItemEnd が none の場合に終端まで再生し切った際の AVPlayerItem の状態が内部でどうなっているのか?については、イマイチ解説されている公式の資料は見つけられなかった。ただ、試してみた感じだと、この終端検知については OS 毎に若干の挙動差があったため、よく検証した上で利用した方が良さそうだった。以下に、発生を確認できた問題を列挙しておく。 AVPlayerItemDidPlayToEndTime が通知されないパターン
HLS 再生にて、AVPlayerItemDidPlayToEndTime が通知されたら先頭までシークすることでループさせた actionAtItemEnd が none の場合だと、2巡目以降で AVPlayerItemDidPlayToEndTime が受け取れなかった
actionAtItemEnd が pause でも、終端到達時に停止判定にならず、バックグラウンドでアプリが生き続けてしまう
iOS 11 で再現せず、iOS 12 で再現するパターンがあった
一応、actionAtItemEnd に none を設定した上で、AVPlayerItemDidPlayToEndTime 検知時に明示的に AVPlayer.pause() すると、先頭までシークする形式のループでも、二巡目以降も問題なく終端検知が行えたが、これが良い方法なのかはわからない。
ループについて
古典的なループの実装方法
が、この方法には以下のような問題がある。
終端まで到達した通知を受け取ってからプレーヤをシーク操作するまでの間に時間がかかるため、レイテンシーが生じる
先頭のメディアデータの再生準備 (WWDC Video では preroll と呼ばれる) が瞬時には行えないので、この再生準備によってもレイテンシーが生じる
最適なループの実装方法
以下の方針で実装すると、このレイテンシーを小さくしつつループを実装できる。
これにより、シームレスなループを実現することができる
2. AVQueuePlayer.currentItem を Observe する
3. currentItem が切り替わったら、古い currentItem を先頭までシークした上で、AVQueuePlayer の末尾に追加する 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 がどんな振る舞いをすれば良いのか決定できないため、だそう。 参考