2018/09/19

Android: Exoplayer DownloadManager

Exoplayer r2.8.4のダウンローダ機能関連APIのメモ.

r2.6.0の頃はコンテンツをダウンロードするAPIだけが提供されていましたが, r2.8.0からはダウンロードタスクの管理やダウンロード処理の再開、サービスや通知といった部分までサポートされるようになっています.

関連記事:Android: ExoPlayer - Downloader

DownloadManager

DownloadManager
マルチダウンロードストリームの管理とダウンロードリクエストの削除をするクラスです.
このクラスのメソッドはメインスレッド上から呼び出す必要があり, 複数スレッドからの呼び出しは想定されていません.

ダウンロードマネージャは内部ハンドラを持ちます. もしLooperを持たないスレッドからの呼び出しがあった場合, Looper.getMainLooper()によってメインスレッドが取得されます.

次のコードはダウンロードマネージャーを生成します.

// ダウンロードアクションファイルのデシリアライズに必要なクラスを定義
private val DOWNLOAD_DESERIALIZERS = arrayOf(
  DashDownloadAction.DESERIALIZER,
  HlsDownloadAction.DESERIALIZER)

fun initDownloadManager() {
  // ダウンローダの生成に必要なコンストラクタヘルパー
  val constructorHelper = DownloaderConstructorHelper(...)

  // ダウンロードマネージャを生成
  DownloadManager(
    constructorHelper,
    DownloadManager.DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
    DownloadManager.DEFAULT_MIN_RETRY_COUNT,
    File( /* アクションファイルを保存するファイルパス */ ),
    DOWNLOAD_DESERIALIZERS)
}

ダウンロードマネージャのコンストラクタパラメータは下記.

public DownloadManager(
    DownloaderConstructorHelper constructorHelper,
    int maxSimultaneousDownloads,
    int minRetryCount,
    String actionSaveFile,
    Deserializer... deserializers) {

constructorHelper
ダウンローダを生成するためのコンストラクタヘルパー.

maxSimultaneousDownloads
最大同時ダウンロード本数. デフォルト値は1 (DownloadManager.DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS) です.

minRetryCount
ダウンロードの最小再試行回数. デフォルト値は5 (DownloadManager.DEFAULT_MIN_RETRY_COUNT) です.

actionSaveFile
DownloadActionのシリアライズを保存するファイルパス. アクションを永続化することでプロセスを跨いでアクションを再開することができる. ただし, このファイルをSimpleCacheで使用したフォルダに保存しないこと. (ダウンローダは知らないファイルを削除するため)

deserializers
actionSaveFileに保存されているDownloadActionのデシリアライザ. HlsDownloadAction.DESERIALIZER etc.

DownloadManager.Listener

DownloadManager.Listenerを使ってダウンロードイベントのリスナーを登録することができます.
次のコールバックメソッドを定義してイベントを受け取ることができるようになります.

onInitialized
全てのアクションがリストアされたときに呼び出される.

onTaskStateChanged
タスクの状態が変わったときに呼び出される.

onIdle
アクティブなタスクがなくなったときに呼び出される.

DownloadAction

DownloadAction
コンテンツのダウンロードリクエストやコンテンツの削除リクエストを表現するクラス.
ダウンロードやコンテンツ削除のために必要なパラメータ情報を保持している.
このクラスは自前のシリアライザ/デシリアライザを持っており, 自身のアクションをシリアライズすることで, プロセスを跨いでも同アクションをでシリアライズして再開できるようになっている.

アクションが持つパラメータは下記.

type
アクションのタイプ.
このタイプ値はシリアライズ情報に含まれ, アクションをデシリアライズする際に最適なデシリアライザーを選択するために使用される.

version
アクションのバージョン.
アクションはシリアライザによって永続化される際にバージョン情報を記録する.
これによって保存されたアクションのバージョンを判別でき, バリデーションやマイグレーション処理に使うことができる.

uri
ダウンロードまたは削除するURI.
SegmentDownloaderを継承したクラスであれば, URIフィールドもシリアライズの対象になる.

isRemoveAction
削除アクションであればtrue, ダウンロードアクションであればfalse.
SegmentDownloaderを継承したクラスであれば, URIフィールドもシリアライズの対象になる.

data
アクションのカスタムデータ.
アクションファイルには任意の情報をカスタムデータとしてバイト配列形式で保存することができる.
SegmentDownloaderを継承したクラスであれば, URIフィールドもシリアライズの対象になる.

アクションファイルのフォーマットは次の通り.

// type, version は共通フォーマット
output.writeUTF(action.type);
output.writeInt(action.version);

// 以下はSegmentDownloadAction系のフォーマット. keysについては後述
output.writeUTF(uri.toString());
output.writeBoolean(isRemoveAction);
output.writeInt(data.length);
output.write(data);
output.writeInt(keys.size());
for (int i = 0; i < keys.size(); i++) 
  writeKey(output, keys.get(i));
}

SegmentDownloader - HlsDownloaderの関係と同じく, HlsDownloadActionSegmentDownloadActionを継承しています.
SegmentDownloadActionの生成方法は下記です.

protected SegmentDownloadAction(
    Uri manifestUri, 
    boolean isRemoveAction, 
    @Nullable String data, 
    K[] keys)

manifestUri
ダウンロードしたいコンテンツのURL(Master/MediaPlaylist etc.).

isRemoveAction
ダウンロードするアクションの場合はfalse, ダウンロードコンテンツの削除アクションの場合はfalse.

data
カスタムデータを指定したい場合はここに指定する.
DownloadServiceでも参照することができる.

keys
ダウンロードするトラックのキー(HLSであればレンディション. DASHであればレプリゼンテーション)を指定します. keysが空配列の場合はすべてのトラックがダウンロードされる.
この引数をnullにすることはできず, またremoveActionがtrueの場合は空配列である必要がある.

DownloadHelper

ダウンロードアクションを生成する際には, ダウンロード対象のプレイリスト/マニフェストURLと, トラックキー(レンディション / レプリゼンテーション)を指定する必要がある.
トラックキーを取得するにはプレイリスト/マニフェストファイルをダウンロード・パースする必要がある.
DownloadHelperはそうした前準備処理とトラックキー取得、ダウンロードアクションの生成を助けてくれる.

DownloadHelperが提供するヘルパーメソッドは次の通り.

prepare
ヘルパーを初期化する.
この操作にはプレイリストやマニフェストのダウンロードを伴う.
引数callbackDownloadHelper.Callbackを指定することで初期化の成功・失敗を受け取ることができる.
初期化処理は別スレッドで実行され, コールバックはメインスレッド上で実行される.

getPeriodCount
有効なピリオドの数を取得します.
HLSコンテンツの場合は固定で1が返され, DASHコンテンツの場合はピリオド数が返されます.
このメソッドはヘルパーを初期化した後で呼び出す必要があります.

getTrackGroups
指定ピリオドに含まれるトラックグループを取得します.
HLSコンテンツの場合, Media playlistであれば空が返され, Master playlistであればvariants, audio, subtitleを含むグループを返します.
DASHコンテンツの場合は, 引数periodIndexで指定されたピリオドに含まれるアダプションセットに含まれるレプリゼンテーションのフォーマット配列を返します.
このメソッドはヘルパーを初期化した後で呼び出す必要があります.

getDownloadAction
指定のトラック(レンディション / レプリゼンテーション)をダウンロードするダウンロードアクションを構築します.
引数dataにはダウンロードアクションのコンストラクタ引数dataを指定します.
このメソッドはヘルパーを初期化した後で呼び出す必要があります.

getRemoveAction
コンテンツを削除するダウンロードアクションを構築します.
このメソッドはヘルパーを初期化していない状態でも呼び出すことができます.

次のコードはヘルパーを使ってダウンロードアクションを生成するものです.

val mediaPlaylistUri = ...
val helper = HlsDownloadHelper(mediaPlaylistUri, dataSourceFactory)
helper.prepare(object : DownloadHelper.Callback {
    override fun onPrepared(helper: DownloadHelper) {
      helper.getDownloadAction( ... )

      // TrackKeyのリストは下記の要領で構築できる
      // val trackKeys = mutableListOf<TrackKey>()
      // for (i in 0 until helper.periodCount) {
      //   val trackGroups = helper.getTrackGroups(i)
      //   for (j in 0 until trackGroups.length) {
      //     val trackGroup = trackGroups.get(j)
      //     for (k in 0 until trackGroup.length) {
      //       // 必要ならtrackGroup.getFormat(k)でパラメータを確認してフィルタアウトできる
      //       trackKeys += TrackKey(i, j, k)
      //     }
      //   }
      // }
    }

    override fun onPrepareError(helper: DownloadHelper, e: IOException) {
      ...
    }
  })

単純にHLSコンテンツのMedia playlistに含まれる全てのレンディションをダウンロードするのであれば, 事前にプレイリストをダウンロードして解析する必要もないので, ヘルパーを使わずに次のように生成します.

HlsDownloadAction(uri, false, data, emptyList())

DonwloadService

DownloadService
バックグラウンドでダウンロード処理を継続維持するためのServiceを継承した抽象クラス.
アプリはこのクラスを継承して必要なメソッドをオーバーライドすることでサービスの管理をExoPlayerに任せることができる.

コンストラクタ引数には次のものがある.

foregroundNotificationId
フォアグラウンドサービス用のNotification ID.

foregroundNotificationUpdateInterval
フォアグラウンドノーティフィケーションをアップデートする間隔(ミリ秒).

channelId
フォアグラウンドノーティフィケーションで使用されるチャネルID.
チャネルは低優先度のチャネルとして作成される. 自身でチャネルを作成する場合はnullを指定する.

channelName
フォアグラウンドノティフィケーションで使用するチャネル名.
自身でチャネルを作成する場合は特に使用されない.

定義されている抽象メソッドは下記.

getDownloadManager()
コンテンツのダウンロードで使用されるDownloadManagerインスタンスを返す.
このメソッドはサービスのライフサイクルの中で1度しか呼ばれない.

getScheduler()
特定の条件を満たした時にDownloadServiceを初期化するジョブを持ったSchedulerを返す.
これによって, アプリが実行されていなくてもダウンロードを開始するスケジューリングが可能になる.
スケジューリングが不要な場合はnullを返す.

getForegroundNotification
フォアグラウンドサービスに必要なNotificationを生成する. 引数taskState[]を使ってNotification情報を構築することができる.
このメソッドは, タスクの状態が変化するか, アクティブなタスクがあれば定期的に呼び出される.
呼び出し間隔はDownloadServiceのコンストラクタで調整可能.
API Lv.26以降, このメソッドはサービスが停止する前に空のTaskState[]を引数に呼び出される.

抽象メソッドではないが, サブクラスが意識するべきメソッドは下記.

getRequirements
ダウンロード開始条件をカスタマイズすることができる. デフォルトではネットワーク接続の有無がダウンロード条件として設定される.

onTaskStateChanged
タスクの状態が変わった時に呼び出される.

ダウンロードサービスの開始

アプリがバックグラウンドにいる状態でもダウンロード処理を継続したい場合は, ダウンロードサービスをフォアグラウンドサービスとして振る舞わせる必要がある.
DownloadServiceDownloadService.startForeground(Notification) を使って起動することができる.

// DownloadService
public static void startWithAction(
    Context context, 
    Class<? extends DownloadService> clazz, 
    DownloadAction downloadAction.
    boolean foreground)

clazz
作成したDownloadServiceのサブクラスを指定します.

downloadAction
DownloadActionはダウンロードストリーム/コンテンツに対するアクション.
対象のストリーム種別によってProgressiveDownloadAction, HlsDownloadAction, DashDownloadActionなどが用意されている.

foreground
フォアグラウンドサービスとして起動する場合はtrue.

ダウンロードサービスを開始するIntentだけが欲しい場合は次のメソッドを使用する.

DownloadService.buildAddActionIntent

生成されるIntentにはダウンロードアクションを格納する download_action と, フォアグラウンドサービスとして移動するかどうかのフラグ foreground が格納される.

ダウンロードサービスは次のメソッドを使うことでダウンロードアクションを指定せずに起動することもできる.

DownloadService.start
DownloadService.startForeground

未完了のダウンロードアクションがある場合や, ダウンロード開始条件が満了された場合, サービスはそれらのダウンロードアクションを再開する.
実行するアクションがなければサービスは即終了する.

ダウンロードタスクの状態

ダウンロードタスクの状態は DownloadManager.TaskState で表現される.

定義:

STATE_QUEUED:開始待ち
STATE_STARTED:開始済み
STATE_COMPLETED:完了済み
STATE_CANCELED:キャンセル済み
STATE_FAILED:失敗

状態遷移図:

queued <-> started -> (canceled | completed | failed)