@gaussbeamの技術ブログ

AVPlayer[1]は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  
    }  

    let itemUrl: URL  

    private var completion: ((Result) -> Void)?  

    private var player: AVPlayer?  
    private var observation: NSKeyValueObservation?  

    init(_ url: URL) {  
        self.itemUrl = url  
    }  

    func load(completion: @escaping (Result) -> Void) {  
        self.completion = completion  
        // AVPlayerを生成し,リソースの取得を行う  
        print("Load: \(self.itemUrl)")  
        self.player = AVPlayer(url: self.itemUrl)  

        self.startObservation()  
   }  

    func startObservation() {  
        guard let remoteItem = self.player?.currentItem else { return }  
        guard self.observation == nil else { return }  

        // `AVPlayer.status`は存在しないファイルURLでもreadyToPlayになるので,`AVPlayer.currentItem.status`の変化を監視  
        self.observation = remoteItem.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)  
    }  
}  

インタフェースを分けた理由

AVPlayerinit(url:)が実行されるとただちにリソースの取得を行います.
(より正確には,URLの情報を持ったAVPlayerItemAVPlayerに紐付けられることによってリソースの取得が行われます[3])
init(_:)load(completion:)に分けることで,抽象リクエストのようにインスタンス生成とリクエストの実行を任意のタイミングで実行できるので,2つのインタフェースを設けました.
(とはいえ,SessionRequestを渡してTaskを生成…といった形式にはなっていないので,AVPlayerRequestとはせず,現状の命名にしています)

2. 任意のタイムアウト時間を設定

ここまでの内容であれば,単なるKVOの追加であるため,責務の分担に目をつぶればViewControllerでやってしまえる範囲内かと思います.
ですが,AVPlayerはタイムアウト時間を任意に設定できないため,通信環境によっては,いつまでも再生が始まらないといったことが起こりえます.
そのため,タイムアウトを設定できるよう,AVPlayerLoaderを以下のように変更します[^4].

1. Enumにタイムアウト状態を追加

    enum Result {  
        …  
+        case timedOut  
    }  

2. タイムアウトインターバルを追加

    let itemUrl: URL  
+    let timeoutInterval: TimeInterval  
   …  
    private var observation: NSKeyValueObservation?  
+        // timerの設定については後述  
+    private var timer: Timer?  

-    init(_ url: URL) {  
+    init(_ url: URL, timeoutInterval: TimeInterval = 5.0) {  
        self.itemUrl = url  
+        self.timeoutInterval = timeoutInterval  
    }  

3. Timerによる独自のタイムアウト処理を追加

    func load(completion: @escaping (Result) -> Void) {  
        …  
        self.startObservation()  
+        self.startTimer()  
    }  

+    @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秒程度で取得可能)

そのため,動画や音声の再生自体が目的である(=リソース自体の取得に時間がかかることも許容されやすい)場合や大容量ファイルの場合には,ストリーミング再生を行うなどより適切な方法での対応が必要かと思います.

参考

[1]: AVPlayer | Apple Developer
[2]: AVFoundationプログラミングガイド
[3]: AVPlayerItem | Apple Developer
[4]: AVPlayerにタイムアウト時間を設定したい | Stack Overflow

この記事へのコメント

まだコメントはありません