はじめに
ExoPlayer ではメディアにあわせて一番良いデコーダーを再生時に自動的に選択してくれるので、
ExoPlayer を利用する時は、デコーダー周りは特に何も気にしないで良いことになっています。
そのはずだったのですが 端末に搭載されるハードウェアのデコーダーの不具合?なのか
再生時に動画が途切れたり、上手くループ再生できなくなるなどの現象に遭遇しました。
その時に ExoPlayer に対してはハードウェアのデコーダーを利用しないように
設定を施したのでその方法を共有したいと思います。
動作を確認するためのサンプルアプリを作る
本記事では次のような構成のアプリを作成していると仮定して設定を進めていきます。
ExoPlayer をインストールする
android {
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.google.android.exoplayer:exoplayer:2.10.8'
︙
}
インターネットにアクセスするのでパーミッションを許可しておく
<?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 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 をセットアップする
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
}
}
デバイスに搭載されているハードウェアのデコーダーを探す
次のコードでデバイスに搭載されているエンコーダーとデコーダーを確認します。
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
クラスを通して、どんなデコーダーが ExoPlayer で利用できるかを取得してます。
通常は MediaCodecSelector.DEFAULT
の実装を利用しており、端末に搭載される全てのデコーダーを返すようになっています。
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.*
で始まるものを取り除く処理を追加します。
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.*
以外のデコーダーを利用して動画を再生するようになります。
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? を参考にしました、
詳しくはこっちで議論されているので確認してみてください。