〈kotlin〉ExoPlayer

Android studioでkotlinを使ってAndroidアプリ作成の勉強中。

Androidで音声・動画の再生に使用できるExoPlayerライブラリを触ってみた。
ExoPlayerはMediaPlayer APIをもとに作られていて、
より多くの機能をサポートしており、拡張やカスタマイズも容易になっている。
exoplayer.dev

今回はこのcodelabをもとにした。
Media streaming with ExoPlayer  |  Android Developers

準備

ExoPlayerはGithubでホストされるオープンソースプロジェクトだったが、
Media3のライブラリに含められたのでMedia3から取得する。

dependencies {
    ...
    implementation "androidx.media3:media3-exoplayer:$mediaVersion"
    implementation "androidx.media3:media3-ui:$mediaVersion"
    implementation "androidx.media3:media3-exoplayer-dash:$mediaVersion"
}

URLからインターネット上の動画でも再生できるようにManifestファイルにpermissionを追加。

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

レイアウト

<androidx.media3.ui.PlayerView
        android:id="@+id/video_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

基本の動画再生

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private var player: ExoPlayer? = null
    
    private var playWhenReady = true //再生・一時停止の状態を保存
    private var currentItem = 0 //メディアアイテムのインデックスを保存
    private var playbackPosition = 0L //再生位置を保存

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }

    /*
    APIレベル24以上では、複数のウィンドウをサポートしており、分割ウィンドウモードではアプリは視認可能だがアクティブにならないため、onStartでプレーヤーを初期化する。
    APIレベル23以下では、アプリがリソースを取得するまでできるだけ長く待機する必要があるため、プレーヤーを初期化する前に onResume まで待つ。
    */
    override fun onStart() {
        super.onStart()
        if(Util.SDK_INT > 23){
            initializePlayer()
        }
    }

    override fun onResume() {
        super.onResume()
        hideSystemUi()   //全画面表示にするために他のUIを非表示にする
        if ((Util.SDK_INT <= 23 || player == null)) {
            initializePlayer()
        }
    }

    private fun hideSystemUi() {
        WindowCompat.setDecorFitsSystemWindows(window, false)
        WindowInsetsControllerCompat(window, binding.videoView).let { controller ->
            controller.hide(WindowInsetsCompat.Type.systemBars())
            controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        }
    }

    private fun initializePlayer(){
        player = ExoPlayer.Builder(this)
            .build()
            .also { exoPlayer ->
                binding.videoView.player = exoPlayer
               
                val mediaItem = MediaItem.fromUri(getString(R.string.media_url_mp4))
                exoPlayer.setMediaItem(mediaItem)
                exoPlayer.playWhenReady = playWhenReady
                exoPlayer.seekTo(currentItem, playbackPosition) //特定のアイテムの特定の位置から開始

                exoPlayer.prepare()
            }
    }

    /*
    APIレベル23以下では、onStop が呼び出される保証がないため、onPause でできるだけ早くプレーヤーを解放する。
    APIレベル24以上では、onStopが呼び出されることが保証される。一時停止状態ではアクティビティが引き続き表示されるため、onStopまで待ってからプレーヤーを解放する。
   */
    override fun onPause() {
        super.onPause()
        if(Util.SDK_INT <= 23){
            releasePlayer()
        }
    }

    override fun onStop() {
        super.onStop()
        if(Util.SDK_INT > 23){
            releasePlayer()
        }
    }

    private fun releasePlayer() {
        //playerを破棄する前に情報を保存しておき、中断したところから再開できるようにする。
        player?.let { exoPlayer ->
            playbackPosition = exoPlayer.contentPosition
            currentItem = exoPlayer.currentMediaItemIndex
            playWhenReady = exoPlayer.playWhenReady
            exoPlayer.release()
        }
        player = null
    }
}

playerは多くのリソースを占有する可能性があり、使用していない時には解放するよう、
playerのライフサイクルをアプリのライフサイクルに関連づけておく(onStart、onResumeでの初期化とonPause、onStopでの解放)。

プレイリストの作成

playerに対してMediaItemを追加することで複数のメディアアイテムの連続再生ができる。

val firstItem = MediaItem.fromUri(firstVideoUri)
val secondItem = MediaItem.fromUri(secondVideoUri)
player.addMediaItem(firstItem)
player.addMediaItem(secondItem)

moveMediaItem, removeMediaItemなどで編集をすることも可能。
詳細はhttps://exoplayer.dev/playlists.html

アダプティブストリーミング

利用可能なネットワーク帯域幅に基づいて動画の再生品質を変え、ユーザーが快適に再生を続けることができるようにするストリーミング。
メディアコンテンツを品質(ビットレートと解像度)が異なる複数のトラックに分割して用意して、プレーヤーが利用可能なネットワーク帯域幅に基づいてトラックを選択する。
アダプティブ ストリーミングにはDASHというフォーマットがよく使われる。
DASH URIにはファイル拡張子がなくMIMEタイプを指定する必要があるため、MediaItem.Builder()を使用してMediaItemを作成する。

private fun initializePlayer() {
    //メディアアイテム内のトラックを選択するtrackSelectorを作成、パラメータで選択条件を指定する
    val trackSelector = DefaultTrackSelector(this).apply {
        setParameters(buildUponParameters().setMaxVideoSizeSd())
    }
     player = ExoPlayer.Builder(this)
            .setTrackSelector(trackSelector)
            .build()
            .also { exoPlayer ->
                binding.videoView.player = exoPlayer
                val mediaItem = MediaItem.Builder()
                    .setUri(getString(R.string.media_url_dash))
                    .setMimeType(MimeTypes.APPLICATION_MPD)
                    .build()

                exoPlayer.setMediaItem(mediaItem)
      ...
}