1. 始める前に

スクリーンショット: ExoPlayer を動画プレーヤーとして使用する Android 向け YouTube アプリ。
ExoPlayer は、Android の低レベルメディア API の上に構築されたアプリレベルのメディア プレーヤーです。ExoPlayer は、YouTube や Google TV などの Google アプリで使用されるオープンソース プロジェクトです。高度なカスタマイズと拡張が可能で、多くの高度なユースケースに対応できます。DASH や SmoothStreaming などのアダプティブ形式などのさまざまなメディア形式をサポートします。
前提条件
- Android 開発と Android Studio に関する適度な知識
演習内容
- さまざまなソースのメディアを準備して再生する
ExoPlayerインスタンスを作成します。 - ExoPlayer をアプリのアクティビティ ライフサイクルと統合して、単一ウィンドウまたはマルチウィンドウ環境でのバックグラウンド処理、フォアグラウンド処理、再生の再開をサポートします。
MediaItemを使用して、プレイリストを作成します。- メディア品質を利用可能な帯域幅に合わせて調整するアダプティブ動画ストリームを再生します。
- イベント リスナーを登録して再生状態をモニタリングし、リスナーを使用して再生の品質を測定する方法を示します。
- 標準の ExoPlayer UI コンポーネントを使用し、さらにアプリのスタイルに合わせてそれらをカスタマイズします。
必要なもの
- Android Studio の最新の安定版と、その使用方法に関する知識。Android Studio、Android SDK、Gradle プラグインが最新版であることを確認してください。
- JellyBean(4.1)以上(理想的には複数のウィンドウをサポートする Nougat(7.1)以上)を搭載した Android デバイス。
2. 設定する
コードを取得する
最初に、Android Studio プロジェクトをダウンロードします。
または、GitHub リポジトリのクローンを作成することもできます。
git clone https://github.com/googlecodelabs/exoplayer-intro.git
ディレクトリ構造
クローンを作成するか ZIP を解凍すると、ルートフォルダ(exoplayer-intro)が作成されます。このフォルダには、複数のモジュール(アプリ モジュールが 1 つと、この Codelab の各ステップに対応するモジュールが 1 つずつ)を含む単一の Gradle プロジェクトに加えて、必要なすべてのリソースが格納されています。
プロジェクトをインポートする
- Android Studio を起動します。
- [File] > [New] > [Import Project] をクリックします。
- ルートの
build.gradleファイルを選択します。

スクリーンショット: インポート時のプロジェクト構造
ビルドが完了すると、6 つのモジュールがあることを確認できます。すなわち、app モジュール(タイプはアプリ)と、exoplayer-codelab-N という名前の 5 つのモジュール(N は 00 から 04, で、それぞれのタイプはライブラリ)です。app モジュールは実際には空で、マニフェストのみが含まれています。app/build.gradle の Gradle 依存関係を使用してアプリをビルドすると、現在指定されている exoplayer-codelab-N モジュール内のすべてが結合されます。
app/build.gradle
dependencies {
implementation project(":exoplayer-codelab-00")
}
メディア プレーヤーのアクティビティは exoplayer-codelab-N モジュールで保持されます。これを別個のライブラリ モジュールで保持するのは、モバイルや Android TV などのさまざまなプラットフォームをターゲットとする APK 間で共有できるようにするためです。それによって、ユーザーが必要とする場合にのみメディア再生機能をインストールできる Dynamic Delivery のような機能を利用することも可能になります。
- アプリをデプロイして実行し、すべてが正常であることを確認します。アプリでは、画面が黒い背景で塗りつぶされます。

スクリーンショット: 黒いアプリが実行される
3.ストリーミングする
ExoPlayer の依存関係を追加する
ExoPlayer は、Jetpack Media3 ライブラリの一部です。各リリースは、次の形式の文字列で一意に識別されます。
androidx.media3:media3-exoplayer:X.X.X
クラスと UI コンポーネントをインポートするだけで、ExoPlayer をプロジェクトに追加できます。ExoPlayer はかなりサイズが小さく、含まれる機能とサポートされる形式に応じて約 70~300 KB の縮小フットプリントがあります。ExoPlayer ライブラリはモジュールに分割されており、デベロッパーは必要な機能のみをインポートできます。ExoPlayer のモジュール構造について詳しくは、ExoPlayer モジュールの追加をご覧ください。
exoplayer-codelab-00モジュールのbuild.gradleファイルを開きます。dependenciesセクションに以下の行を追加し、プロジェクトを同期します。
exoplayer-codelab-00/build.gradle
def mediaVersion = "1.0.0-alpha03"
dependencies {
[...]
implementation "androidx.media3:media3-exoplayer:$mediaVersion"
implementation "androidx.media3:media3-ui:$mediaVersion"
implementation "androidx.media3:media3-exoplayer-dash:$mediaVersion"
}
PlayerView element を追加する
exoplayer-codelab-00モジュールから、レイアウト リソース ファイルactivity_player.xmlを開きます。FrameLayout要素の内部にカーソルを置きます。<PlayerViewと入力し始めると、Android Studio によってPlayerView要素がオートコンプリートされます。widthとheightにはmatch_parentを使用します。- ID を
video_viewとして宣言します。
activity_player.xml
<androidx.media3.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
これ以降は、この UI 要素を動画ビューと呼びます。
PlayerActivityで、編集したばかりの XML ファイルから作成されたビューツリーへの参照を取得できます。
PlayerActivity.kt
private val viewBinding by lazy(LazyThreadSafetyMode.NONE) {
ActivityPlayerBinding.inflate(layoutInflater)
}
- ビューツリーのルートを、アクティビティのコンテンツ ビューとして設定します。また、
videoViewプロパティがviewBinding参照で視認可能であることと、そのタイプがPlayerViewであることを確認します。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(viewBinding.root)
}
ExoPlayer を作成する
ストリーミング メディアを再生するには、ExoPlayer オブジェクトが必要です。これを作成する最も簡単な方法は、ExoPlayer.Builder クラスを使用することです。その名前が示すとおり、このクラスはビルダー パターンを使用して ExoPlayer インスタンスを作成します。
ExoPlayer は、Player インターフェースの便利な多目的の実装です。
ExoPlayer を作成するために、プライベート メソッド initializePlayer を追加します。
PlayerActivity.kt
private var player: ExoPlayer? = null
[...]
private fun initializePlayer() {
player = ExoPlayer.Builder(this)
.build()
.also { exoPlayer ->
viewBinding.videoView.player = exoPlayer
}
}
コンテキストを使用して ExoPlayer.Builder を作成した後、build を呼び出して ExoPlayer オブジェクトを作成します。次に、それを player に割り当てます。これはメンバー フィールドとして宣言する必要があります。さらに、viewBinding.videoView.player 可変プロパティを使用して、player を対応するビューにバインドします。
メディア アイテムを作成する
player には、再生するコンテンツが必要です。そのために、MediaItem を作成します。MediaItem にはさまざまなタイプがありますが、最初はインターネット上の MP3 ファイル用のものを作成します。
MediaItem を作成する最も簡単な方法は、メディア ファイルの URI を受け入れる MediaItem.fromUri を使用することです。player.setMediaItem を使用して、MediaItem を player に追加します。
alsoブロック内のinitializePlayerに次のコードを追加します。
PlayerActivity.kt
private fun initializePlayer() {
[...]
.also { exoPlayer ->
[...]
val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3))
exoPlayer.setMediaItem(mediaItem)
}
}
R.string.media_url_mp3 は、strings.xml で https://storage.googleapis.com/exoplayer-test-media-0/play.mp3 として定義されていることに注意してください。
アクティビティのライフサイクルで適切に再生する
player は、メモリ、CPU、ネットワーク接続、ハードウェア コーデックなどの多くのリソースを占有する可能性があります。特に 1 つしかないハードウェア コーデックでは、これらのリソースの多くが不足します。アプリがリソースを使用していないとき(バックグラウンドで実行されているときなど)に、これらのリソースを解放して他のアプリが使用できるようにすることが重要です。
言い換えると、プレーヤーのライフサイクルをアプリのライフサイクルに関連付ける必要があります。この関連付けを実装するには、PlayerActivity の 4 つのメソッド(onStart、onResume、onPause、onStop)をオーバーライドします。
PlayerActivityを開いた状態で、[Code menu] > [Override methods...] をクリックします。onStart、onResume、onPause、onStopを選択します。- API レベルに応じて、
onStartまたはonResumeコールバックでプレーヤーを初期化します。
PlayerActivity.kt
public override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
initializePlayer()
}
}
public override fun onResume() {
super.onResume()
hideSystemUi()
if ((Util.SDK_INT <= 23 || player == null)) {
initializePlayer()
}
}
API レベル 24 以上の Android は、複数のウィンドウをサポートします。アプリは視認可能ですが、分割ウィンドウ モードではアクティブにならないため、onStart でプレーヤーを初期化する必要があります。API レベル 23 以下の Android では、アプリがリソースを取得するまでできるだけ長く待機する必要があるため、プレーヤーを初期化する前に onResume まで待ちます。
hideSystemUiメソッドを追加します。
PlayerActivity.kt
@SuppressLint("InlinedApi")
private fun hideSystemUi() {
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowInsetsControllerCompat(window, viewBinding.videoView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
hideSystemUi は onResume で呼び出されるヘルパー メソッドであり、これにより全画面エクスペリエンスを実現できます。
onPauseとonStopで、releasePlayer(まもなく作成します)を使用してリソースを解放します。
PlayerActivity.kt
public override fun onPause() {
super.onPause()
if (Util.SDK_INT <= 23) {
releasePlayer()
}
}
public override fun onStop() {
super.onStop()
if (Util.SDK_INT > 23) {
releasePlayer()
}
}
API レベル 23 以下では、onStop が呼び出される保証がないため、onPause でできるだけ早くプレーヤーを解放する必要があります。API レベル 24 以上(マルチウィンドウ モードと分割ウィンドウ モードが導入されています)では、onStop が呼び出されることが保証されます。一時停止状態では、アクティビティが引き続き表示されるため、onStop まで待ってからプレーヤーを解放します。
次に、releasePlayer メソッドを作成する必要があります。このメソッドはプレーヤーのリソースを解放して破棄します。
- アクティビティに次のコードを追加します。
PlayerActivity.kt
private var playWhenReady = true
private var currentItem = 0
private var playbackPosition = 0L
[...]
private fun releasePlayer() {
player?.let { exoPlayer ->
playbackPosition = exoPlayer.currentPosition
currentItem = exoPlayer.currentMediaItemIndex
playWhenReady = exoPlayer.playWhenReady
exoPlayer.release()
}
player = null
}
プレーヤーを解放して破棄する前に、次の情報を保存します。
- 再生 / 一時停止状態(
playWhenReadyを使用)。 - 現在の再生位置(
currentPositionを使用)。 - 現在のメディア アイテム インデックス(
currentMediaItemIndexを使用)。
以上により、ユーザーによって中断された位置から再生を再開できるようになります。そのために必要なのは、プレーヤーの初期化時にこの状態情報を提供することだけです。
最終準備
ここで必要なのは、releasePlayer に保存した状態情報を初期化時にプレーヤーに提供することだけです。
initializePlayerに次の行を追加します。
PlayerActivity.kt
private fun initializePlayer() {
[...]
exoPlayer.playWhenReady = playWhenReady
exoPlayer.seekTo(currentItem, playbackPosition)
exoPlayer.prepare()
}
次のことが起こります。
playWhenReadyは、再生用のリソースがすべて取得されたらすぐに再生を開始するかどうかをプレーヤーに指示します。playWhenReadyの初期値はtrueなので、アプリが初めて実行されたときは自動的に再生が開始されます。seekToは、特定のメディア アイテム内の特定の位置までシークするようプレーヤーに指示します。currentItemとplaybackPositionはどちらもゼロに初期化されるので、アプリが初めて実行されたときは最初から再生が開始されます。prepareは、再生に必要なリソースをすべて取得するようプレーヤーに指示します。
音声を再生する
これで完了です。アプリを起動して MP3 ファイルを再生し、埋め込みアートワークを確認してください。

スクリーンショット: 単一のトラックを再生しているアプリ。
アクティビティのライフサイクルをテストする
アクティビティ ライフサイクルのさまざまな状態すべてでアプリが動作するかどうかをテストします。
- 別のアプリを起動し、アプリをフォアグラウンドに戻します。正しい位置から再開されるでしょうか?
- アプリを一時停止し、バックグラウンドに移動した後、再度フォアグラウンドに戻します。一時停止状態でバックグラウンドに移動した場合、一時停止状態のままになるでしょうか?
- アプリを回転させます。向きを縦向きから横向きに変更してから元に戻すと、どのように動作するでしょうか?
動画を再生する
動画を再生したい場合は、メディア アイテムの URI を MP4 ファイルに変更するだけで、簡単に対応できます。
initializePlayerの URI をR.string.media_url_mp4に変更します。- アプリを再起動し、音声の場合と同様に、動画再生をバックグラウンドに移動した後の動作をテストします。
PlayerActivity.kt
private fun initializePlayer() {
[...]
val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4))
[...]
}
PlayerView がすべてを処理します。アートワークではなく、動画が全画面表示されます。

スクリーンショット: 動画を再生しているアプリ。
これで完成です。ライフサイクル管理、状態保存、UI コントロールを備え、Android で全画面メディア ストリーミングを行うアプリを作成できました。
4. プレイリストを作成する
現在のアプリは単一のメディア ファイルを再生しますが、複数のメディア ファイルを連続で再生したい場合はどうすればよいでしょうか?そのためには、プレイリストが必要です。
プレイリストを作成するには、addMediaItem を使用して複数の MediaItem を player に追加します。そうすればシームレスな再生が可能になり、バッファリングがバックグラウンドで処理されるため、メディア アイテムの変更時にバッファリング スピナーがユーザーに表示されなくなります。
initializePlayerに次のコードを追加します。
PlayerActivity.kt
private void initializePlayer() {
[...]
exoPlayer.addMediaItem(mediaItem) // Existing code
val secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3))
exoPlayer.addMediaItem(secondMediaItem)
[...]
}
プレーヤー コントロールの動作をチェックします。
と
を使用して、連続したメディア アイテム間を移動できます。

スクリーンショット: 「次へ」ボタンと「前へ」ボタンが表示された再生コントロール
これはかなり便利です。詳しくは、メディア アイテムおよびプレイリストに関するデベロッパー ドキュメントと、Playlist API に関するこちらの記事をご覧ください。
5. アダプティブ ストリーミング
アダプティブ ストリーミングは、利用可能なネットワーク帯域幅に基づいてストリームの品質を変化させながら、メディアをストリーミングする手法です。これにより、ユーザーは帯域幅が許容する最高品質のメディアを体験できます。
通常、同じメディア コンテンツは、品質(ビットレートと解像度)が異なる複数のトラックに分割されます。プレーヤーは、利用可能なネットワーク帯域幅に基づいてトラックを選択します。
各トラックは、特定の期間(通常は 2~10 秒)のチャンクに分割されます。これにより、プレーヤーは利用可能な帯域幅の変化に応じて、トラックをすばやく切り替えることができます。プレーヤーは、シームレスな再生を行うためにこれらのチャンクを結合する処理を行います。
アダプティブなトラック選択
アダプティブ ストリーミングの核心は、現在の環境に最も適したトラックを選択することにあります。アダプティブなトラック選択を使用してアダプティブ ストリーミング メディアを再生するように、アプリを更新します。
- 次のコードで
initializePlayerを更新します。
PlayerActivity.kt
private fun initializePlayer() {
val trackSelector = DefaultTrackSelector(this).apply {
setParameters(buildUponParameters().setMaxVideoSizeSd())
}
player = ExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build()
[...]
}
最初に DefaultTrackSelector を作成します。これは、メディア アイテム内のトラックを選択する役割を果たします。次に、標準画質以下のトラックのみを選択するよう trackSelector に指示します。これは、品質を犠牲にしてユーザーのデータを保存するための良い方法です。最後に、trackSelector をビルダーに渡して、ExoPlayer インスタンスの作成時に使用されるようにします。
アダプティブな MediaItem を作成する
DASH は、広く使用されているアダプティブ ストリーミング形式です。DASH コンテンツをストリーミングするには、これまでと同様に MediaItem を作成します。ただし、今回は fromUri の代わりに MediaItem.Builder を使用する必要があります。
なぜなら、fromUri は基盤となるメディア形式を決定するためにファイル拡張子を使用しますが、DASH URI にはファイル拡張子がないため、MediaItem を作成する際に APPLICATION_MPD の MIME タイプを指定する必要があるからです。
initializePlayerを次のように更新します。
PlayerActivity.kt
private void initializePlayer() {
[...]
// Replace this line...
val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4));
// ... with this
val mediaItem = MediaItem.Builder()
.setUri(getString(R.string.media_url_dash))
.setMimeType(MimeTypes.APPLICATION_MPD)
.build()
// Keep this line
exoPlayer.setMediaItem(mediaItem)
// Remove the following lines
val secondMediaItem = MediaItem.fromUri(getString(R.string.media_url_mp3))
exoPlayer.addMediaItem(secondMediaItem)
}
- アプリを再起動し、DASH でアダプティブな動画ストリーミングが機能していることを確認します。ExoPlayer を使うと実に簡単です。
その他のアダプティブ ストリーミング形式
よく使用されるその他のアダプティブ ストリーミング形式としては HLS(MimeTypes.APPLICATION_M3U8)と SmoothStreaming(MimeTypes.APPLICATION_SS)があり、どちらも ExoPlayer でサポートされています。その他のアダプティブ メディアソースの作成方法については、ExoPlayer デモアプリを参照してください。
6. イベントをリッスンする
これまでのステップでは、プログレッシブなメディア ストリームとアダプティブなメディア ストリームをストリーミングする方法を学びました。ExoPlayer は、舞台裏で次のような多くの作業を行っています。
- メモリの割り当て
- コンテナ ファイルのダウンロード
- コンテナからのメタデータの抽出
- データのデコード
- 画面とスピーカー システムへの動画、音声、テキストのレンダリング
ユーザーの再生エクスペリエンスを理解して改善するにあたっては、ExoPlayer が実行時に何をしているかを知ることが役立つ場合があります。
たとえば、次のような方法で再生状態の変化をユーザー インターフェースに反映させたい場合があります。
- プレーヤーがバッファリング状態になったときに読み込みスピナーを表示する
- トラックが終了したときに「次を再生」オプションを含むオーバーレイを表示する
ExoPlayer は、有用なイベントのコールバックを提供するいくつかのリスナー インターフェースを備えています。リスナーを使用すると、プレーヤーがどのような状態にあるかをログに記録できます。
リッスンする
PlayerActivityクラスの外部でTAG定数を作成します。これは後でロギングに使用します。
PlayerActivity.kt
private const val TAG = "PlayerActivity"
PlayerActivityクラスの外部で、ファクトリ関数にPlayer.Listenerインターフェースを実装します。これは、エラーや再生状態の変化といった重要なプレーヤー イベントを通知するために使用します。- 次のコードを追加して
onPlaybackStateChangedをオーバーライドします。
PlayerActivity.kt
private fun playbackStateListener() = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
val stateString: String = when (playbackState) {
ExoPlayer.STATE_IDLE -> "ExoPlayer.STATE_IDLE -"
ExoPlayer.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
ExoPlayer.STATE_READY -> "ExoPlayer.STATE_READY -"
ExoPlayer.STATE_ENDED -> "ExoPlayer.STATE_ENDED -"
else -> "UNKNOWN_STATE -"
}
Log.d(TAG, "changed state to $stateString")
}
}
PlayerActivityで、タイプがPlayer.Listenerのプライベート メンバーを宣言します。
PlayerActivity.kt
class PlayerActivity : AppCompatActivity() {
[...]
private val playbackStateListener: Player.Listener = playbackStateListener()
}
onPlaybackStateChanged は、再生状態が変化したときに呼び出されます。新しい状態は playbackState パラメータで提供されます。
プレーヤーは次の 4 つの状態のいずれかになります。
状態 | 説明 |
| プレーヤーはインスタンス化されていますが、まだ準備されていません。 |
| 十分なデータがバッファリングされていないため、プレーヤーは現在の位置から再生することができません。 |
| プレイヤーは現在の位置からすぐに再生できます。これは、プレーヤーの playWhenReady プロパティが |
| プレーヤーはメディアの再生を終了しました。 |
リスナーを登録する
コールバックが呼び出されるためには、playbackStateListener をプレーヤーに登録する必要があります。これは initializePlayer で行います。
- 再生の準備をする前にリスナーを登録します。
PlayerActivity.kt
private void initializePlayer() {
[...]
exoPlayer.seekTo(currentWindow, playbackPosition)
exoPlayer.addListener(playbackStateListener)
[...]
}
繰り返しになりますが、プレーヤーからの宙ぶらりんの参照(これはメモリリークを引き起こす可能性があります)を回避するために、コードを整理する必要があります。
releasePlayer内のリスナーを削除します。
PlayerActivity.kt
private void releasePlayer() {
player?.let { exoPlayer ->
[...]
exoPlayer.removeListener(playbackStateListener)
exoPlayer.release()
}
player = null
}
- logcat を開き、アプリを実行します。
- UI コントロールを使用して、再生をシークし、一時停止し、再開します。ログで再生状態の変化を確認できます。
もっと知識を深める
ExoPlayer は、ユーザーの再生エクスペリエンスを理解するために役立つリスナーを他にもいくつか備えています。たとえば、音声と動画のリスナーや、すべてのリスナーからのコールバックを含む AnalyticsListener があります。最も重要なメソッドを次にいくつか示します。
onRenderedFirstFrameは、動画の最初のフレームがレンダリングされたときに呼び出されます。これを使用して、意味のあるコンテンツが画面に表示されるまでにユーザーが待つ必要がある時間を計算できます。onDroppedVideoFramesは、動画フレームがドロップしたときに呼び出されます。ドロップしたフレームは、再生中にジャンクが発生し、ユーザー エクスペリエンスが低下している可能性を示します。onAudioUnderrunは、音声のアンダーランが発生したときに呼び出されます。アンダーランはサウンドに可聴グリッチを生じさせ、ドロップした動画フレームよりもユーザーが気付きやすくなります。
AnalyticsListener は、addListener で player に追加できます。音声リスナーと動画リスナーにも、対応するメソッドがあります。
Player.Listener インターフェースには、プレーヤーの状態が変化したときにトリガーされる、より一般的な onEvents コールバックも含まれています。これが役立つケースとしては、複数の状態変化に同時に応答する場合や、複数の異なる状態変化に同様に応答する場合などがあります。個別の状態変更コールバックの代わりに onEvents コールバックの使用が必要となる場合のその他の例については、リファレンス ドキュメントをご覧ください。
アプリとユーザーにとってどのようなイベントが重要かを検討してください。詳しくは、プレーヤー イベントのリッスンをご覧ください。イベント リスナーについては以上です。
7. 完了
これで、ExoPlayer をアプリに統合する方法について多くのことを習得しました。
詳細
ExoPlayer について詳しく知るには、デベロッパー ガイドとソースコードを確認し、ExoPlayer ブログを購読してください。