前回(Jetpack Compose, ExoPlayer3, HLSでAndroid TV用のアプリを作成した)に引き続き
ExoPlayerのデフォルトUIはスマートフォンでは割とうまく動くけどD-Pad入力しかないAndroid TVではうまく使えず、結局UIを自作する必要があるので作りました・・・
あと動画の先読み(バッファリング)もやった
ソースコード
https://gitlab.com/mkiuchi/exoplayer-demo/-/tree/forwardbuffer?ref_type=heads
できたもの
1. 動画の先読み(バッファリング)
CMや気に入らないシーンをばばばと飛ばすためには先読みバッファはあったほうがいい。
DefaultLoadControlを使う。ドキュメントを素直に読んでるとDownload Service使うのかなーと思ったりする(し作例もぽつぽつある)けど、これはデバイス内に動画をダウンロードして再生するオフライン再生機能なので今回やりたいことではない。
以下のような感じにする。各オプションの説明はドキュメントを参照。
// バッファ秒数
val forwardBufferMs: Int = 15 * 60 * 1000
val backBufferMs: Int = 10 * 60 * 1000
// バッファの設定
val loadControl = DefaultLoadControl.Builder()
.setBufferDurationsMs(
/* minBufferMs = */ 50000, // プレイヤーが再生を開始するまでバッファする秒数(default=50000)
/* maxBufferMs = */ forwardBufferMs, // 最大バッファ秒数(default=50000)
/* bufferForPlaybackMs = */ 2500, // シーク後に再生を再開するために必要なバッファ秒数(default=2500)
/* bufferForPlaybackAfterRebufferMs = */ 5000 // リバッファの際に再生を再開するために必要な秒数(default=5000)
)
.setBackBuffer(
/* backBufferDurationMs = */ backBufferMs, // 巻き戻し用バッファの秒数(default=0)
/* retainBackBufferFromKeyframe = */ false // バッファの確保をキーフレームから行うかどうか(default=false)
)
.build()
// ExoPlayerインスタンスの作成
ExoPlayer.Builder(context)
.setSeekBackIncrementMs(15*1000) // 巻き戻し時の秒数
.setSeekForwardIncrementMs(15*1000) // 早送り時の秒数
.setLoadControl(loadControl) // バッファをセット
.build()
.apply {
// (略)
}
明確な説明はないけど、たぶんメモリ上にバッファとして蓄えられるのでI/OとしてはDownload Serviceで使う内蔵ストレージよりは速いはず。はずなんだけど、実デバイスで使ってみるともっさり感が否めない。多分CPUが貧弱なのではないかと思う。Android StudioのDevice Emulatorはかなり速いので、それを基準にして作ってしまうと実デバイスで実行したときにあれ?となる。Android TVのYouTubeアプリはシーク時に動画は一時停止したままで、サムネイルを変化させることでシーク位置をユーザに決めさせている。このほうがサクサク感あるかもしれない。
maxBufferMs
は設定しても設定値を再生開始直後から全力バッファする動作にはなっていないみたい。動画の再生がすすむにつれで最大バッファ量までバッファするようになる。なんとかしたい場合はminBufferMs
, bufferForPlaybackMs
をいじればいいのかなと思う。
2. UI
で、UIを作る。お手本としてみんなおなじみJetStreamComposeをじーっと見て、使う。けど、よくわかんないので自分なりに削ぎ落としたものにして、少しYouTubeアプリに寄せてみた。
Composableの構成は以下の図のように入れ子になっている。D-Padの入力を受け付けるコンポーザブルと、その結果が反映されるコンポーザブルが異なるところが少しややこしいところ。
2.1. D-Pad の入力を受け付け
これはほぼJetStreamComposeのコピペ。カスタム修飾子(Modifier)を使ってModifier.dPadEvents
, Modifier.handleDPadKeyEvents
, Modifier.handleDPadEvents
関数を作成してD-Padの入力イベントに対応した動作を定義する。なんでModifier.handleDPadKeyEvents(onPreviewKeyEvent)
, Modifier.handleDPadEvents(onKeyEvent)
の2つを定義しているかは(コメントには解説があるけど)よくわからなかった。
D-Padの上、右、左、中央を押すとコントローラを可視化するメソッドを呼び出す。右か左を押すと動画のシーク(早送り、巻き戻し)を行う。中央を押すと動画の一時停止、再開を行う。
2.2. コントローラの可視化
ControllerState
クラスをインスタンス化したものをrememberで保存しておいて、可視化するときにはshowControls()
メソッドを呼び出す。showControls()
では内部変数_controlsVisible
をtrueにセットする。
そうすると、MovieController
コンポーザブルの中のstate
にも反映されるので、state.controlsVisible
の変化をトリガとして、CinematicBackground
コンポーザブルとMovieControls
(の親)コンポーザブルが可視化される。
2.3. 一定時間なにも操作しなかったらコントローラを不可視に変更
ControllerState
の中にはChannelが定義してあって、showControl()
呼び出し時に指定した秒がフローとしてobserve()
内で処理され、指定秒数経過後に、内部変数_controlsVisible
をfalseにセットすることでコントローラを不可視に変更する。
2.4. 再生状態のアップデート
これはMainActivityの中で無限ループで値を取得して、子コンポーザブルに渡している。
3. おわりです
Googleプロダクトあるあるだけど、サンプルが凝ってててこずった・・・。なんかShaka Playerのことを思い出したわ笑