［Android］ExoPlayer でハードウェアのデコーダーを利用しないようにする

# はじめに

ExoPlayer ではメディアにあわせて一番良いデコーダーを再生時に自動的に選択してくれるので、
ExoPlayer を利用する時は、デコーダー周りは特に何も気にしないで良いことになっています。

そのはずだったのですが 端末に搭載されるハードウェアのデコーダーの不具合？なのか
再生時に動画が途切れたり、上手くループ再生できなくなるなどの現象に遭遇しました。

その時に ExoPlayer に対してはハードウェアのデコーダーを利用しないように
設定を施したのでその方法を共有したいと思います。

# 動作を確認するためのサンプルアプリを作る

本記事では次のような構成のアプリを作成していると仮定して設定を進めていきます。

![Nov-29-2019 23-20-18.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79387/0ae7c688-92ea-246a-8ce8-9dd696442c56.gif)

## ExoPlayer をインストールする

```groovy:build.gradle(app)
android {
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com.google.android.exoplayer:exoplayer:2.10.8'
        ︙
}
```

## インターネットにアクセスするのでパーミッションを許可しておく

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="kaleidot725.exoplayersample">
    <uses-permission android:name="android.permission.INTERNET"/>
        ︙
</manifest>
```

## ExoPlayer の PlayerView をレイアウトする

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:show_buffering="when_playing"
        app:show_shuffle_button="true"/>

</FrameLayout>
```

## ExoPlayer をセットアップする

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val view = findViewById<PlayerView>(R.id.player_view)
        val player = ExoPlayerFactory.newSimpleInstance(this).apply {
            val uri = Uri.parse("https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4")
            val dataSource = DefaultDataSourceFactory(this@MainActivity, Util.getUserAgent(this@MainActivity, "ExoPlayerSample"))
            val videoSource = ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri)
            prepare(videoSource)
        }
        view.player = player
    }
}
```

# デバイスに搭載されているハードウェアのデコーダーを探す

次のコードでデバイスに搭載されているエンコーダーとデコーダーを確認します。

```kotlin
    MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos.forEach { codecInfo ->
        Log.d("CODEC_INFO", "media ${codecInfo.name} ")
    }
```

次のような感じでメディアごとのデコーダーとエンコーダーが出力されます。
これは Pixel3 XL で実行したものですが `c2.android.*` と `OMX.google.*` と2種類のエンコーダーとデコーダーがあります。

```
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media c2.android.avc.decoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media OMX.google.h264.decoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media c2.android.avc.encoder 
2019-11-30 10:21:01.741 6207-6207/kaleidot725.exoplayersample D/CODEC_INFO: media OMX.google.h264.encoder
```

調べたところ`c2.android.*` が Pixel3 に搭載されたハードウェアのエンコーダーとデコーダー、
`OMX.google.*` が Android OS で実装されたソフトウェアのエンコーダーとデコーダーを指しているようです。
今回はハードウェアのデコーダーを利用したくないので、`c2.android.avc.decoder` が対象になります。

# ハードウェアのデコーダーを利用しないようにする

ExoPlayer では [`MediaCodecSelector`](https://github.com/donparapidos/ExoPlayer/blob/master/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java) クラスを通して、どんなデコーダーが ExoPlayer で利用できるかを取得してます。
通常は [`MediaCodecSelector.DEFAULT`](https://github.com/donparapidos/ExoPlayer/blob/master/library/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecSelector.java) の実装を利用しており、端末に搭載される全てのデコーダーを返すようになっています。

```java:MediaCodecSelector.java
  MediaCodecSelector DEFAULT =
      new MediaCodecSelector() {
        @Override
        public List<MediaCodecInfo> getDecoderInfos(
            String mimeType, boolean requiresSecureDecoder, boolean requiresTunnelingDecoder)
            throws DecoderQueryException {
          return MediaCodecUtil.getDecoderInfos(
              mimeType, requiresSecureDecoder, requiresTunnelingDecoder);
        }

        @Override
        public @Nullable MediaCodecInfo getPassthroughDecoderInfo() throws DecoderQueryException {
          return MediaCodecUtil.getPassthroughDecoderInfo();
        }
      };
```

今回はこの `MediaCodecSelector` クラスをカスタマイズすることで
ハードウェアのデコーダーを ExoPlayer で利用しないようにします。

次のように `getDecoderInfo` で利用できるデコーダーを取得する処理に 
`name` が `c2.android.*`で始まるものを取り除く処理を追加します。

```kotlin
        val customCodecSelector = object : MediaCodecSelector {
            override fun getDecoderInfos(mimeType: String?, requiresSecureDecoder: Boolean, requiresTunnelingDecoder: Boolean): MutableList<MediaCodecInfo> {
                return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder).toMutableList().filter {
                    !it.name.startsWith("c2.android.")
                }.toMutableList()
            }

            override fun getPassthroughDecoderInfo(): MediaCodecInfo? {
                return MediaCodecUtil.getPassthroughDecoderInfo()
            }
        }
```

そして ExoPlayer を生成するときにこのクラスのインスタンスを設定すれば、
ExoPlayer では `c2.android.*` 以外のデコーダーを利用して動画を再生するようになります。

```kotlin
        val view = findViewById<PlayerView>(R.id.player_view)
        val trackSelector = DefaultTrackSelector()
        val renderersFactory = DefaultRenderersFactory(this).apply {
            setMediaCodecSelector(customCodecSelector)
        }
        val player = ExoPlayerFactory.newSimpleInstance(this, renderersFactory, trackSelector).apply {
            val uri = Uri.parse("https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4")
            val dataSource = DefaultDataSourceFactory(this@MainActivity, Util.getUserAgent(this@MainActivity, "ExoPlayerSample"))
            val videoSource = ProgressiveMediaSource.Factory(dataSource).createMediaSource(uri)
            prepare(videoSource)
        }
        view.player = player
```

うまく設定できていれば指定したハードウェアを利用せずに動画を再生していることがわかるような次のログが出でてきます。

```
2019-11-30 11:58:49.588 3862-3941/kaleidot725.exoplayersample I/MediaCodec: [OMX.google.h264.decoder] setting surface generation to 3954689
```

# おわりに

ハードウェアのデコーダーを利用しないようにしましたが、
ソフトウェアのデコーダーを利用するとCPU使用率が上昇するので注意が必要だったりします。
そのため本来であれは ExoPlayer にデコーダーの選択など全てを任せるのが安心できます。

ですがデバイスだとうまくデコードできないという場面に直面した場合は本実装が有効なので、
デバイス依存の問題には直面してしまったときはこういった実装でしのげるかなと思います。

# 参考記事

本記事は [Can we tweak hardware/software decoding in Exoplayer?](https://github.com/google/ExoPlayer/issues/3039) を参考にしました、
詳しくはこっちで議論されているので確認してみてください。