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 ブログを購読してください。