Androidマンになりたいおじさん

自分の備忘録的なのです。Androidを普及できたらいいなと思ったりしながら書きます

【入門】ExoPlayerと仲良くなっていかない?

はじめに

こんにちは!!!!最近花粉で死にかけてます!!!! 12月くらいから、Androidに変わりはないんですが動画周りを扱う専門チームに希望して異動し、日々楽しく過ごしております!!!

Androidももちろんですが、動画単体の記事などをたくさんあげていけたらいいなとおもいます〜〜〜〜

ということで、ExoPlayerと仲良くなるために入門していきましょう

ExoPlayerってなーに?

  • application labelで使えるAndroidで動画や音楽を扱うためのMediaPlayer OSS Library
  • 既存でMediaPlayer APIがあるじゃん
  • ExoPlayerは4.1以降じゃないと使えない!でももう5.0くらいからサポートしてしまえば、よさそうやな?
  • DASH, HLS, SmoothStreaming, 共通暗号化などにも対応してる
  • 優れた設計により、カスタマイズ性, 拡張性が高いということ

こんな親切なサイトまで用意されてる!軽く目を通して見るといいかも? 全く分からんわー!って人はこの記事を読み終わってから、もう一回見に行ってもいいかも!

exoplayer.dev

ExoPlayerのCodelabsが用意されてるので、これを通して、入門していきましょう!!!!

codelabs.developers.google.com

Let's Exo

ExoPlayer導入

みんな大好きGithub

github.com

でわ、AndroidStudioで適当な新規プロジェクトを作っていきましょう!

2020/03/04 現在の一番新しいのにしてみましょう

implementation 'com.google.android.exoplayer:exoplayer-core:2.11.3'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.11.3'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.3'

ExoPlayerは基本はcoreが必須でそれ以外は使用用途に合わせて、各moduleを追加していったらいいみたいですね! 試しに追加した、dashはDASHサポートをするためで、UIはプレイヤー再生するためのUI Componentsを提供してくれるみたい!

じゃあ、HLSをサポートしたかったら、exoplayer-hlsとか入れちゃえばいいってことですね!

何が必要かよく分からんし、全部入れちゃえばいいのでは????????

そういうときは

implementation 'com.google.android.exoplayer:exoplayer:2.11.3’

これだけでよきよきです

このままじゃ、うまく動かないのでJava8解禁しましょう

compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
}

Player View設置

これで準備ができたので、まずはPlayerを表示するためのViewを設置しましょう

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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/video_view"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        />

</androidx.constraintlayout.widget.ConstraintLayout>

さっきui componentを入れといたおかげで、これだけでlayoutは完結しちゃいます...すごい...

実際にcodeでplayerを生成して、video_viewに紐づけていきましょう

private lateinit var playerView: PlayerView
private var mediaPlayer: SimpleExoPlayer? = null

playerView = video_view
initPlayer()

private fun initPlayer() {
    mediaPlayer = SimpleExoPlayer.Builder(this).build()
    playerView.player = mediaPlayer
}

最新verの一番SimpleなPlayerの作り方になります!
Builderにcontextを渡すだでおっけーみたい!

2.11.xでインスタンス生成に大きな変更が出ています。2.11より前のversionでは別途記事を参照くださいmm

このBuilderの引数や、Builder.setHogeHogeで色んなものが渡せるみたいで、基本は渡さなくても全部ExoPlayerが内部で用意してくれているDefaultHogeHogeが使われる形になりそう!
このDefaultHogeHogeを自分で改造したり、都合のいいように拡張してあげて、渡すだけで容易にカスタマイズできそう!

細かい種類の深堀りは後日記事で一つずつ見ていこうと思います!

(2020/03/09現在にその内の1つ、LoadControlについて書きました)

gumiossan.hatenablog.com

現段階でbuildすると何も機能しないThe Player!!っぽいのが表示されましたね???

MediaSourceの作り方

実際に動画を再生するために動画コンテンツデータを表現するMediaSourceを作っていきます!!!

これもSimpleにDefaultDataSourceFactoryを作り、それをProgressive(HLS, Dash..etc.)MediaSourceを作るときに渡してやったら、mp4を再生してみます!(mp3も可)

適当なsample urlをstring resourceとして用意します! 今回は動画に関わったら、嫌というほど見かけるBigBunnyを借りましょう! https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4

指定のuriを渡したら、MediaSourceを返してくれるメソッドでも定義しておきましょう

private fun buildMediaSource(uri: Uri): MediaSource {
    val dataSourceFactory = DefaultDataSourceFactory(this, "exoplayer-sample-app")
    return ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
}

ExoPlayerはしっかり面倒みよう

ExoPlayerは何でも出来ちゃう優秀な子なんですが、使用する際にAndroidのLifecycleに合わせて、うまく適切な呼び出しをやってあげないとだめな子です....

この辺はクライアント側で自由にやらせてくれるって意味もありますが、
勝手にやってくれる訳ではないので、適切に書いていきますます

身構えなくてもよいです!playerのインスタンスが空だったら、触らない!createやstartのタイミングでnull checkして、nullならインスタンス生成してあげましょう!

逆にstopやpauseなどでplayerインスタンスが存在する時だけ、不必要になったら関連づいたリソースをreleaseしてあげて、playerインスタンスをnullにsetしてあげたりたり!


private fun releasePlayer() {
    mediaPlayer?.let {
        playWhenReady = it.playWhenReady
        playbackPosition = it.currentPosition
        currentWindow = it.currentWindowIndex
        it.release()
        mediaPlayer = null
    }
}

ExoPlayer周りの値

おいおいおい!しれっと知らん値が出てきてるぞー!ってならないでください(

  • playWhenReadyはplayerが再生するリソースもばっちり読み込んで、問題なくいつでも再生できるよ!ということを返してくれます
  • playbackPositionは現在再生してる動画の位置ですね。これを記憶して、seekTo()とかに渡してあげたら前回の再生位置とか復帰できそうですね!
  • currentWindowは現在再生しているコンテンツの長さを表現するようなものだと今は思っていたらよさそうです!

ExoPlayerはTimeline, Window, Periodという抽象的な概念でmediaというのを表現しています。これは今回深堀りしません、後日書きます

再生してみよう

先程宣言したinitPlayerも少し変えて、実際にuriをparseしてbuildMediaSourceでmediaSourceを再生してみましょう!

作成したmediaSourceををmediaPlayer.prepare()に渡してあげて、動画の準備が出来次第(playWhenReady)再生できます!簡単!すごい!

private fun initPlayer() {
    val uri = Uri.parse(getString(R.string.media_url_mp4))
    val mediaSource = buildMediaSource(uri)

    mediaPlayer = SimpleExoPlayer.Builder(this).build().apply {
        playWhenReady = playWhenReady
        seekTo(currentWindow, playbackPosition)
        prepare(mediaSource, false, false)
    }

    playerView.player = mediaPlayer
}

lifecycleでどのタイミングでinitやreleaseを呼ぶか、currentWindowなどの変数は自分で宣言してください!!!!!

ここまで出来ると無事皆さんもBigBunnyとご対面ですね!!わいわい

プレイリストみたいなことがしたい

でも、今のままじゃ1つの動画しか再生できませんね?? 複数の動画はどうしたらいいのだろう???

複数のMediaSourceをまとめるConcatenatingMediaSourceがあります! これは最近は用語として当たり前になってきた(?)、playlist的なのを作成することを可能にしてくれるはずです!

buildMediaSourceを以下のように変えてみましょう

private fun buildMediaSource(): MediaSource {
    val dataSourceFactory = DefaultDataSourceFactory(this, "exoplayer-sample-app")
    val uri = listOf<Uri>(
        Uri.parse(getString(R.string.media_url_mp4)),
        Uri.parse(getString(R.string.google_sample_url)),
        Uri.parse(getString(R.string.media_url_mp4)),
        Uri.parse(getString(R.string.google_sample_url)),
        Uri.parse(getString(R.string.media_url_mp4)),
        Uri.parse(getString(R.string.google_sample_url))
    )

    val mediaSourceList = mutableListOf<MediaSource>().apply {
        uri.forEach {
            val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(it)
            add(mediaSource)
        }
    }

    return ConcatenatingMediaSource(*mediaSourceList.toTypedArray())
}

複数のMediaSourcesを作成して、それを可変長なConcatenatingMediaSourceに渡しています これだけでプレイリストができちゃいます!wow

工夫次第で自分だけのプレイリストを作ることができるような気がしてきません???あ、しないですか。。はい

ExoPlayerだからこそ、できたこと

次はAdaptive Streamingをやっていきましょう! Adaptive Streamingはざっくりいうと、ユーザーのネット環境などに合わせて柔軟に最適なStreamを勝手に調整してくれるものです(wow

以下の記事を書いたので、詳しくはこちらへ gumiossan.hatenablog.com

つまり、配信サーバー側で各画質のStreamを用意することで、高速wifiに繋いだりすると1080pのFHDのものを再生し、LTEなどに切り替わったら、720pのHDに変わったり、通信環境が最悪になると320pとかに変えてくれるようなことを指します

これらのStreaming規格の代表的なのがDASHやHLSといったものになります

実装難度かなり高そうなことを実現するためにExoPlayerはに内部にTrackSelectorというものがいます

これをSimpleExoPlayerを生成するときにプラスで指定してやりましょう

val trackSelector = DefaultTrackSelector(this).apply {
    setParameters(buildUponParameters().setMaxVideoSizeSd())
}

mediaPlayer = SimpleExoPlayer.Builder(this)
    .setTrackSelector(trackSelector)
    .build()
    .apply {
        playWhenReady = playWhenReady
        seekTo(currentWindow, playbackPosition)
        prepare(mediaSource, false, false)
    }

DefaultTrackSelectorはDefaultでAdaptiveTrackSelectionを生成してくれているので、これで大丈夫ですb

先程のmp4を適当なsample dashのuriに変えてあげて、DashMediaSource.Factoryに変えてあげるだけです!分かりやすくていいですねb

private fun buildMediaSource(): MediaSource {
    val dataSourceFactory = DefaultDataSourceFactory(this, "exoplayer-sample-app")
    val uri = Uri.parse(getString(R.string.media_url_dash))

    return DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
}

これでdashコンテンツが再生できるはずですが、いまいちすごさが分からないですね???

Network Link Conditionerという帯域を自由に絞ることができるツールを使って、Wifiから3Gに絞ったときのビデオ品質をみてみたいと思います!
ちょっと以下の画像じゃわかりにくいですが、3Gではそれに見合ったStreamが再生されていて、字幕がすごく見にくいことがわかりますね

f:id:qurangumio:20200306001508p:plain
3G
f:id:qurangumio:20200306001505p:plain
Wifi

でわ、AdaptiveTrackSelection内部で帯域を見積もる役割をしてる、BandWidthMeterのbitrateEstimateをwifiから3Gに切り替えた時にどのくらい値が変わってるかをみてみましょう 動画再生中にWifi -> 3Gに切り替えたら、以下のようにbitrateEstimateがスムーズに切り替わりました

wifi: 1288781 
3G: 339707

教えて、ExoPlayer!

ExoPlayerにはあらゆるユースケースを満たすためののListenerがたくさん存在します それらを少しみていきましょう!

mediaPlayer.add~って入力したら、補完にたくさんそれらしいのが出てきますね??

恐らく、たくさん(?)使いそうなPlayer.EventListenerの以下3つを実装してみましょう!


private val playerEventListener = object : Player.EventListener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        val state = if (isPlaying) "STATE_PLAYING" else "STATE_NOT_PLAYING"
        Log.d(TAG, "isPlayingChanged: $state")
    }

    override fun onPlayerError(error: ExoPlaybackException) {
        Log.d(TAG, "Error: ${error.message ?: ""}")
    }

    override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
        val stateStr = when(playbackState) {
            SimpleExoPlayer.STATE_IDLE -> "STATE_IDLE"
            SimpleExoPlayer.STATE_BUFFERING -> "STATE_BUFFERING"
            SimpleExoPlayer.STATE_READY -> "STATE_READY"
            SimpleExoPlayer.STATE_ENDED ->  "STATE_ENDED"
            else -> "UNKNOWN"
        }

        Log.d(TAG, "state is $stateStr, playWhenReady is $playWhenReady")
    }
}

とりあえず動かして、色々操作をしながらLog出力とかをみてみると勉強になるかもしれません!

あ、Listener登録はprepareの前にしておきましょう!じゃないと適切なstateが検知できなくなります、、、

アプリ起動 -> state is STATE_BUFFERING, playWhenReady is false
Bufferが溜まってきた -> state is STATE_READY, playWhenReady is false
再生 -> state is STATE_READY, playWhenReady is true && isPlayingChanged: true && isPlayingChanged: STATE_PLAYING
停止 -> state is STATE_READY, playWhenReady is false
シーク操作(蓄えてたbufferをぶっ飛ばします) -> state is STATE_BUFFERING, playWhenReady is false
最後まで見る -> state is STATE_ENDED, playWhenReady is true

再生するプレイリストを意味のわからない文字列に変えて、rebuildしてみましょう
アプリ起動 -> state is STATE_IDLE, playWhenReady is false

再生中にわざとネット切断してみる -> Error: com.google.android.exoplayer2.upstream.HttpDataSource$HttpDataSourceException: Unable to connect to url

ここで有名なExoの図を貼っておきます

f:id:qurangumio:20200306002216p:plain
ExoPlayer State

IDLE -> 何を再生したらいいかわからないよー = 再生できるものがない BUFFERING -> バッファリング中 READY -> 再生可能ですよー! END -> 再生完了しきった!

この辺をみておけば、Playerのstateとかをうまく管理できそうですね! お気づきかもしれませんが、playWhenReady && READYのときは再生中ということになりますね!
前まではこうやって見ていたのですが新しくisPlayingChangedが追加されたので、これをみておけば、再生開始されてるかどうかを検知できます

Errorはそのままのとおりで、Exo内で何かしらErrorを検知したい場合にって感じですね

最後にreleaseのタイミングでしっかり、登録したlistenerはremoveしておきましょう!じゃないとmemory leakしちゃいますからね….

少しだけPlayerViewについて

最後に少しだけPlayerViewをみましょう!


app:use_controller="false"

これをするとプレイヤーのコントローラがみえなくなります
VODならまだしも、LIVEとかだといらないですからね、、、



元に戻して、次は以下を追加してみましょう
基本単位はmsです!

app:show_timeout="10000"
 -> 指定秒数アプリに触れていなかったら、コントローラがhideになります
app:fastforward_increment="30000"
 -> 早送りできます
app:rewind_increment="30000"
 -> 巻き戻しできます



さいごに

割と長くなってしまったので、この記事はここまで!!!!

でも一気に入門できた気になった気がしませんか???そんなことないですか??

実際に手を動かして、読んでくれた人はきっと自分でも気づいていないけど、わかっているかもしれません(?)

最初はふんわりとした理解でいいと思います...ExoPlayerは巨大なので、必要になった箇所を深掘って学んでいけばいいのかな〜〜〜なんて

最後にここまでで書いたコードはあげていますので、ご自由にどぞ!

github.com

なので、ほんとに基本の基本は押さえれていると思います!!!!!保証しませんけどね!!!!!!!
それでは皆さん、よいExo lifeを!!!!!!