AVPlayerのリソース取得結果を扱いやすくする
AVPlayer1はURLを渡すだけでリモートやバンドル内からリソースを取得してくれます.
が,AVPlayer内部でリクエストが処理されるため,単なるAPIリクエストの実行のような扱い方はできません.
そこで,一般的なAPIリクエストに近しい使い勝手で処理できるようにしてみました.
サンプルは以下にあります.
gaussbeam/AVPlayerLoaderSample
この記事では,以下のポイントについて説明します.
1. リクエストの実行結果のハンドリング
2. 任意のタイムアウト時間を設定
1. リクエストの実行結果のハンドリング
リソース取得の成功・失敗は,AVPlayer.currentItem.status
により把握できます2.
このプロパティは変更を監視できるので,変更されたタイミングでクロージャを実行することで,実行結果を外部からハンドリングできます.
なお,AVPlayer
自体にもstatus
というプロパティもありますが,この値は存在しないファイルURLでもreadyToPlayになるので,上記の値を監視しています.
final class AVPlayerLoader {
enum Result {
case success(AVPlayer)
case failed
}
var itemUrl: URL {
return (self.playerItem.asset as! AVURLAsset).url
}
private let playerItem: AVPlayerItem
private var player: AVPlayer?
private var observation: NSKeyValueObservation?
private var completion: ((Result) -> Void)?
init(_ url: URL) {
self.playerItem = AVPlayerItem(url: url)
}
func load(completion: @escaping (Result) -> Void) {
self.completion = completion
self.startObservation()
// AVPlayerを生成し,リソースの取得を行う
print("Load: \(self.itemUrl.absoluteString)")
self.player = AVPlayer(playerItem: self.playerItem)
}
func startObservation() {
guard self.observation == nil else { return }
// `AVPlayer.status`は存在しないファイルURLでもreadyToPlayになるので,`AVPlayerItem.status`の変化を監視
self.observation = self.playerItem.observe(\.status) { item, change in
switch item.status {
case .readyToPlay:
print("Completed")
self.finishLoading(.success(self.player!))
case .failed:
print("Failed")
self.finishLoading(.failed)
case .unknown:
// unknownは初期値のため,この処理は実行されない
break
}
}
}
func finishLoading(_ result: Result) {
self.observation?.invalidate()
self.observation = nil
self.completion?(result)
}
}
インタフェースを分けた理由
AVPlayer
はイニシャライザが実行されるとただちにリソースの取得を行います.
(より正確には,AVURLAsset
を持ったAVPlayerItem
がAVPlayer
に紐付けられることによってリソースの取得が行われます3)
init(_:)
とload(completion:)
に分けることで,抽象リクエストのようにインスタンス生成とリクエストの実行を任意のタイミングで実行できるので,2つのインタフェースを設けました.
(とはいえ,Session
にRequest
を渡してTask
を生成…といった形式にはなっていないので,AVPlayerRequest
とはせず,現状の命名にしています)
2. 任意のタイムアウト時間を設定
ここまでの内容であれば,単なるKVOの追加であるため,責務の分担に目をつぶればViewControllerでやってしまえる範囲内かと思います.
ですが,AVPlayerはタイムアウト時間を任意に設定できないため,通信環境によっては,いつまでも再生が始まらないといったことが起こりえます.
そのため,タイムアウトを設定できるよう,AVPlayerLoaderを以下のように変更します4.
1. Enumにタイムアウト状態を追加
enum Result {
…
+ case timedOut
}
2. タイムアウトインターバルを追加
…
+ let timeoutInterval: TimeInterval
private let playerItem: AVPlayerItem
…
private var observation: NSKeyValueObservation?
+ // timerの設定については後述
+ private var timer: Timer?
- init(_ url: URL) {
+ init(_ url: URL, timeoutInterval: TimeInterval = 5.0) {
self.playerItem = AVPlayerItem(url: url)
+ self.timeoutInterval = timeoutInterval
}
3. Timer
による独自のタイムアウト処理を追加
func load(completion: @escaping (Result) -> Void) {
…
self.startObservation()
+ self.startTimer()
…
self.player = AVPlayer(playerItem: self.playerItem)
}
+ @objc func didTimeout() {
+ print(message: "Timed out")
+ self.finishLoading(.timedOut)
+ }
+
+ func startTimer() {
+ self.timer = Timer.scheduledTimer(
+ timeInterval: timeoutInterval,
+ target: self,
+ selector: #selector(didTimeout),
+ userInfo: nil,
+ repeats: false)
+ }
func finishLoading(_ result: Result) {
+ self.timer?.invalidate()
+ self.timer = nil
…
self.completion?(result)
}
このようにすることで,失敗時やタイムアウト時にはエラーメッセージの表示やローカルリソースへの差し替えが可能になります.
(サンプルでは,1つ前の画面でリソースを先読みしておき,取得に成功したらビデオ再生画面に遷移する,というケースを想定して実装しています)
備考
この記事の内容は,以下のような状況を想定しています.
- リソース自体の再生自体が目的ではない(=そのための待ちやエラー表示は許容できない)
- e.g. オンボーディングやヘルプなどのちょっとした説明に動画を使う
- ファイルサイズが小さい
- 検証で用いたファイルは100KB程度(3G相当の環境でも1.5秒程度で取得可能)
そのため,動画や音声の再生自体が目的である(=リソース自体の取得に時間がかかることも許容されやすい)場合や大容量ファイルの場合には,ストリーミング再生を行うなどより適切な方法での対応が必要かと思います.
参考
Author And Source
この問題について(AVPlayerのリソース取得結果を扱いやすくする), 我々は、より多くの情報をここで見つけました https://qiita.com/gaussbeam/items/9b7e8e598ac198531d00著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .