2018/10/19

Android: ExoPlayer DownloadManager, DownloadService

ExoPlyerのDownloadManager, DownloadServiceを調べた時のメモ.

中断されたアクションを読み込むタイミング

保存されたActionFileの読み込みタイミングはDownloadManagerを初期化したタイミングとなります.

プロセスの強制終了などでActionを完遂できなかった場合, 保存されたActionFileをプロセス再開後に読み込んでダウンロード処理を再開する必要があります.
保存されたActionFileは DownloadManager.loadActions によってバックグラウンドスレッド上で読み込まれ, これはDownloadManagerのコンストラクタで実行されます.

コンテンツの削除はバックグラウンド?

コンテンツを削除するにはremove flagをtrueにしたアクションを発行します.
アクションはDownloadServiceで実行されるので, フォアグラウンドサービスとして実行することが可能です.
削除中の通知も DownloadService.getForegroundNotification で返すNotification objectをカスタマイズすることができます.
また, 通知の雛形として DownloadNotificationUtil.buildProgressNotification が用意されています.
buildProgressNotification は引数のタスクステートに応じて通知の表示内容を変えるため, ダウンロード中通知や削除中通知もこのメソッドで生成できます.

ダウンロード進捗率を取得する

DownloadManager.Listenerの onTaskStateChanged コールバック引数のTaskStateから間近のダウンロード進捗率が取得できます.
TaskState.downloadPercentage がダウンロード進捗率を格納したフィールドです.
ダウンロード進捗率が未定/不明 あるいは 削除タスクである場合は com.google.android.exoplayer2.C#PERCENTAGE_UNSET がセットされます.
DownloadServiceでは, このコールバックを受けて通知の進捗率を更新するようになっています.

タスクの実行順序とダウンロードのキャンセル

タスクはDownloadManagerによってArrayListで管理されており, 新しいタスクはタスクキュー(タスクリスト)の最後尾に追加され, 先頭から順に実行されます.
DownloadManager.handleAction は新しいダウンロード/削除アクションアクションからタスクを生成してタスクキューの最後尾に追加します.
新しく削除アクションがリクエストされた場合, 既に同じメディアファイルのダウンロードタスクがタスクキューに存在するなら, そのダウンロードを即座にキャンセルします.
つまり, ダウンロード中やダウンロードリクエストをキューイングした後にこれをキャンセルしたい場合は削除アクションを投げるとキャンセルできます.

ダウンロードの開始条件

サービスによっては”従量制ネットワーク接続時にはダウンロードしたくない”といった要件があるかもしれません.
あるいは, “NWが瞬断されてダウンロード中断されたけれど, NW接続が回復したら自動再開したい” 要件があるかもしれません.
ExoPlayer Downloaderではデバイスの状態を監視して, こうした要件に応える機能があります.

RequirementsHelper はデバイスの状態を監視し, 特定の条件を満たした場合にダウンロードを開始/再開するヘルパークラスです. ここで指定できる”特定の条件” は Requirements クラスで表現され, Requirementsに指定できる条件は次の通りです.

  1. ネットワーク種別 ( NW接続済み, 従量制NWに接続済み, ローミング中, etc. )
  2. 充電中かどうか
  3. アイドル状態かどうか

また, JobSchedulerによる監視もサポートされています.
JobSchedulerを使用する場合は, Requirementsの情報がJobInfoに変換されてスケジューリングされます.

API Lv.によっては判定できるNW種別の種類や, アイドル状態と判定する条件に差異があるので, JobSchedulerやRequirementsのコードを確認した方がよいです.

ダウンロードの開始プロセス

ダウンロードを開始するにはDownloadManagerを初期化して, DownloadServiceを起動し, タスク(アクション)を追加する必要があります.
DownloadServiceは起動されるとRequirementsHelperを起動してデバイス状態を監視し始めます.
デバイスの状態がダウンロード開始条件を満たした場合, いよいよダウンロード処理が開始されます.

RequirementsHelperとスケジューラの生存区間

アプリのプロセスが生きている間はRequirementsHelperが動的ブロードキャストレシーバーを使ってデバイス状態を監視し, ダウンロードを開始/再開させます.
デバイスの監視はDownloadServiceによって開始されますが, ダウンロードが中断されてDownloadServiceが停止してもこの監視は続きます.
これは, デバイスの状態を監視するRequirementsHelperをDownloadServiceのstaticフィールドで保持しているためです.

DownloadServiceがgetSchedulerでスケジューラを指定している場合は, スケジューラでもデバイス状態が監視されます.
スケジューラはAndroid標準のJobSchedulerを使用することができ, これによってアプリのプロセスが停止している場合にもダウンロードを開始/再開させることが可能になります.
スケジューラはダウンロード開始条件が満たされていないと判断された場合にスケジューリングされます.

ダウンロード開始条件が満たされた場合, RequirementsHelperによる監視が続いている(アプリのプロセスが生きている)状態であればRequirementsHelperがダウンロードを開始/再開させて, スケジューラのスケジューリングをキャンセルします.
RequiermentsHelperによる監視がされていない(アプリのプロセスが停止している)状態であればスケジューラによる開始/再開が行われます.

スケジューラだけでデバイス監視しないのは, RequirementsHelper(staticフィールドと動的ブロードキャストレシーバー)を使った方がデバイス状態の検知からダウンロードの開始/再開までを素早く行えるというメリットがあります.

RequirementsとSchedulerとDownloadService

RequirementsとSchedulerは, DownloadServiceのgetSchedulerとgetRequirementsをオーバーライドして指定します.

  @Override
  protected Requirements getRequirements() {
    return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
  }

  @Override
  protected PlatformScheduler getScheduler() {
    return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
  }

JobScheduler を使ったデバイスの監視を実現するため PlatformScheduler クラスが用意されています. PlatformSchedulerを使うにはAndroidManifest.xmlに次の定義を追加します.

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

<service android:name="com.google.android.exoplayer2.util.scheduler.PlatformScheduler$PlatformSchedulerService"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true"/>

あるいはFirebaseJobDispatcherを使ったスケジューラ JobDispatcherScheduler も用意されています. JobDispatcherSchedulerを使うにはAndroidManifest.xmlに次の定義を追加します.

  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

  <service
      android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
      android:exported="false">
    <intent-filter>
      <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
    </intent-filter>
  </service>
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の関係と同じく, HlsDownloadActionはSegmentDownloadActionを継承しています.
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
ヘルパーを初期化する.
この操作にはプレイリストやマニフェストのダウンロードを伴う.
引数callbackにDownloadHelper.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
タスクの状態が変わった時に呼び出される.

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

アプリがバックグラウンドにいる状態でもダウンロード処理を継続したい場合は, ダウンロードサービスをフォアグラウンドサービスとして振る舞わせる必要がある.
DownloadService は DownloadService.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)