注意!
この記事はQiitaにて公開されていた内容をimportしたものです。
これらの内容は場合によっては陳腐化していて役に立たなくなっていたり、有害であったり、現在の著者の主張と異なることがあります。
皆様の判断の上でご利用いただけますと幸いです(度を超してヤバいものは著者に連絡して頂ければ対応します m(_ _)m)
はじめに
本ドキュメントを、運命に抗えず動画再生案件に投げ込まれた全ての人に捧ぐ
とは言いつつも備忘録なので誰かに役立つかどうかはわかりません。あと急に加筆などするかもしれませんので悪しからず。
この記事を書いた後にExoPlayerを扱う案件から遠ざかっていたのですが、数年ぶりにExoPlayerを扱うサービスにジョインした為、大規模改訂を行いました。
本記事で扱うバージョン
- ExoPlayer
- 2.9.6
- Kotlin
- 1.3.30
com.google.android.exoplayer2.Player.EventListener
Player
インターフェースとして定義されている addListener(EventListener)
から EventListener をセットすることで、コールバックを受け取れるようになります。
PlayerインターフェースはSimpleExoPlayer
クラスにて実装されているので、普通にExoPlayerFactory.newSimpleInstance()
したインスタンスであればリスナーを利用することができます。
それぞれのコールバックメソッドを実際に使ってみたJavaDocと感想文は以下の通りです。
void onLoadingChanged(boolean isLoading)
/**
* Called when the player starts or stops loading the source.
*
* @param isLoading Whether the source is currently being loaded.
*/
ローカルファイルやHLSの取得状態が変化したときコールされる。 HLSなどはデータを事前読み込みしながら再生することになるので、1つのMediaSourceで何度も状態が変化することになる。 HLSとかの通信の発生するデータ読み込みでProgressを出したりするとき便利かも知れない。
- isLoading
- ソースから取得している状態かどうか
void onPlayerStateChanged(boolean playWhenReady, int playbackState)
/**
* Called when the value returned from either {@link #getPlayWhenReady()} or {@link
* #getPlaybackState()} changes.
*
* @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants.
*/
playWhenReady
か playbackState
が変化するとコールされる(とJavaDocに書かれている)。
- playWhenReady
- trueのとき準備が完了次第MediaSourceが再生される。falseなら再生されない。
- ExoPlayer#setPlayWhenReady(boolean) で制御できます
- playbackState
- ExoPlayer.STATE_BUFFERING
- バッファリング中
- ExoPlayer.STATE_ENDED
- メディア(MediaSource)を再生しきった状態
- この状態からREADYに戻す場合、
Player#seekTo(C.TIME_UNSET)
をするとか、新しいMediaSourceをセットするとよい
- ExoPlayer.STATE_IDLE
- MediaSourceを持っていない状態のため再生も一時停止もできない状態
- ExoPlayer.STATE_READY
- 再生可能な状態
- ExoPlayer.STATE_BUFFERING
void onTimelineChanged(Timeline timeline, @Nullable Object manifest, @TimelineChangeReason int reason)
/**
* Called when the timeline and/or manifest has been refreshed.
*
* <p>Note that if the timeline has changed then a position discontinuity may also have
* occurred. For example, the current period index may have changed as a result of periods being
* added or removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity(int)}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
* @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/
Timeline か Manifest が更新されたときにコールされる。 Timelineが変更されて再生位置の不連続が起きたときも実行されることに注意。TimelineからPeriodが追加・削除された結果として変更された可能性があるとき等。
とまぁなんか分かったような分からなかったようなことが書いてあるんですが、(シンプルにローカルファイルやオンデマンドファイルを再生しているだけならば)ほぼこのコールバックは使わないのではないか?と思います。
ところでManifestって何?という疑問はあるのですが、(僕が調査した当時は)ExoPlayerライブラリ内でも該当するものはDashManifest
だけのようでした。
私は残念ながらDASHを触っていないのでこれ以上の追求は出来ませんでした・・・。
余談: Timelineに含まれるWindowとPeriodについて
Timeline
とはメディアの「時刻表」(文字通りのタイムテーブル)であるが、大雑把にMediaSourceのはじまりからおわりだと思うことにする。
MediaSourceには原則としてTimelineと(場合によっては)Manifestが一対ずつある。
ところがConcatenatingMediaSourceのように複数のTimelineを抱えるものもある(これをConcatenatedTimeline
としてひとつのTimelineにラップし直している)。
Timelineには更にPeriod
及びWindow
の概念が含まれる。
Periodは単一のメディアファイルなどの論理的なメディアを指す。また広告が含まれるか、どこに含まれるかも定義されている。
Windowはシークをサポートしているかや、どこまで再生できるかを表現している。
静的なメディア(端末内のMP3とか、HLS配信される(ライブ配信ではない)オンデマンド配信コンテンツ)の場合、ひとつのTimelineにはひとつずつのWindow及びPeriodが入っていることになる。
ライブ配信のような終わりが不明なメディアについては、C.TIME_UNSET
な長さのPeriodとなり、WindowについてはisDynamic=true
な状態で可変しながら再生が続くことになるでしょう(変化したら都度 onTimelineChanged
がコールされるかも知れませんが、都合の良いライブソースがなかったので検証できてません)
void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections)
/**
* Called when the available or selected tracks change.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
* @param trackSelections The track selections for each renderer. Never null and always of
* length {@link #getRendererCount()}, but may contain null elements.
*/
トラックが変更されたときコールされる。
ひとつのMediaSourceのメディアに含まれる音声・映像・字幕・メタデータのそれぞれを指している概念が Format
である。
Formatはそれぞれのタイプに対して複数存在することもある。例えばHLSだったらAdaptive Streamingの為に動画の形式がビットレート別に複数提供されていることもある。
最適なFormatを選択する仕組みがTrackSelectorで、アルゴリズムを調整したければ ExoPlayerFactory.newSimpleInstance
の引数でカスタムしたTrackSelectorを与えると良い。
trackGroups
- 現在のメディア(Period)に属するFormatのArrayを格納する
TrackGroup
の配列(を格納しているだけのTrackGroupArray
)を返す
- 現在のメディア(Period)に属するFormatのArrayを格納する
trackSelections
TrackSelector
などで選択されたFormat(だけじゃなくて便宜上全部もっているんだけど。)を保持するTrackSelection
の配列(を格納しているだけのTrackSelectionArray
)を返す
余談: トラックの戻る・進むを検知する
Player#seekTo(nextWindowIndex, C.TIME_UNSET)
など、つまりトラックの戻る・進むなどをしたときにコールされることを確認。
更新前の本記事で onTimelineChanged
を用いたConcatenatingMediaSourceのPrev/Nextの検知を紹介したが、以下のように実装することで実現できるようになった。
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
val empty = if (trackGroups == TrackGroupArray.EMPTY) "(empty)" else ""
val currentPeriodIndex = player.currentPeriodIndex
val currentWindowIndex = player.currentWindowIndex
Timber.d("onTracksChanged: groups: %h%s, selections: %h, period: %d, window: %d",
trackGroups, empty, trackSelections, currentPeriodIndex, currentWindowIndex)
if (currentPeriodIndex > previousPeriodIndex) {
onTracksChangedFastForward()
} else if (currentPeriodIndex < previousPeriodIndex) {
onTracksChangedRewind()
}
previousPeriodIndex = currentPeriodIndex
}
void onRepeatModeChanged(@RepeatMode int repeatMode)
/**
* Called when the value of {@link #getRepeatMode()} changes.
*
* @param repeatMode The {@link RepeatMode} used for playback.
*/
リピートモードが変更されたときコールされる。
リピート機能はそれ以外のMediaSourceでも使えるかも知れませんが ConcatenatingMediaSource
を使っているとき特に便利に機能する気がします。
see: Repeat modes in ExoPlayer – google-exoplayer – Medium
- repeatMode
- REPEAT_MODE_OFF
- 繰り返しなしの通常の再生を行う
- REPEAT_MODE_ONE
- 今再生しているWindowを無制限に繰り返し再生する
- REPEAT_MODE_ALL
- 今再生しているTimelineを無制限に繰り返し再生する
- REPEAT_MODE_OFF
void onShuffleModeEnabledChanged(boolean shuffleModeEnabled)
/**
* Called when the value of {@link #getShuffleModeEnabled()} changes.
*
* @param shuffleModeEnabled Whether shuffling of windows is enabled.
*/
シャッフルモードの有効・無効が切り替わったときコールされる・・・ というか、RepeatModeもそうだけど、MediaSource側からではなく、ExoPlayer側から制御できるようになったのか・・・。知らなかった・・・。 僕みたいにならないように、みんなはExoPlayerの公式Mediumを購読しような!
void onPlayerError(ExoPlayerException error)
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and {@link
* #release()} must still be called on the player should it no longer be required.
*
* @param error The error.
*/
エラーが起きたときコールされる。
JavaDocに書いてあるとおり、これがコールされたときIDLE状態になるのが注意点。
どういうときにエラーがコールされるかはExoPlayerException
を見ると手がかりになりそう
void onPositionDiscontinuity(@DiscontinuityReason int reason)
/**
* Called when a position discontinuity occurs without a change to the timeline. A position
* discontinuity occurs when the current window or period index changes (as a result of playback
* transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
*
* <p>When a position discontinuity occurs as a result of a change to the timeline this method
* is <em>not</em> called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
* case.
*
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
Timelineの変更のない状態で、再生位置の不連続が起きたときコールされる。
JavaDocに書いてあるようなケースの時は onTimelineChanged
がコールされ、こちらのコールバックはコールされない。
雑に言うと再生位置が飛ぶとコールされる。 (例えば、シークバーが動かされる、10秒早く送るボタンを押すなど)
- reason
- DISCONTINUITY_REASON_PERIOD_TRANSITION
- 再生中にTimeline内のPeriodをまたいだ時
- ひとつの音源をループさせる
LoopingMediaSource
を使っているとき、現在のPeriodのindex(Player#getCurrentPeriodIndex()
)は同じままこのreasonがコールされる
- DISCONTINUITY_REASON_SEEK
- シークさせたとき(Period内でも、別のPeriodになってもシークされたならこの理由でコールされる)
- DISCONTINUITY_REASON_SEEK_ADJUSTMENT
- リクエストされた位置にシークできないとき、または不正確であることが許可されているとき(?)
Seek adjustment due to being unable to seek to the requested position or because the seek was permitted to be inexact.
- 一部の動画フォーマットでIフレームにしかシークできないようなものだったとき、要求された位置にはシークできないので、これがコールされるのかな・・・?とか思いました
- DISCONTINUITY_REASON_AD_INSERTION
- 広告のポジションに入ったとき、この理由でコールされる
- DISCONTINUITY_REASON_INTERNAL
- 内部的な理由で呼ばれるやつ、こういうのツラい
- DISCONTINUITY_REASON_PERIOD_TRANSITION
void onPlaybackParametersChanged(PlaybackParameters playbackParameters)
/**
* Called when the current playback parameters change. The playback parameters may change due to
* a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
* them (for example, if audio playback switches to passthrough mode, where speed adjustment is
* no longer possible).
*
* @param playbackParameters The playback parameters.
*/
[PlaybackParameters](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/PlaybackParameters.html)
が変更されたときにコールされる。
PlaybackParametersは、 speed
, pitch
, skipSilence
のフィールドを持っている(いつの間にそんなことが出来るようになっていたんだ・・・)
void onSeekProcessed()
/**
* Called when all pending seek requests have been processed by the player. This is guaranteed
* to happen after any necessary changes to the player state were reported to {@link
* #onPlayerStateChanged(boolean, int)}.
*/
シークが実行されたときにコールされる。 実行順が面白いケースがある。 HLSコンテンツの時、シークバーを触ってシークさせたときにシークポジションの差分取得が終わっていないことがあるが、 そのときは差分取得が終わってからシークされるため、このコールバックもそれを待つ形になる。 我々の感覚としてシークバーを動かした直後にコールバックされるのかな?と思ってしまうが、そういうことではないので気をつけたい。
またJavaDocにあるように、onPlayerStateChanged
の後に呼ばれることが保証されているとのこと。
しかしどういうユースケースがあるのかはちょっと思いつかなかったです。