HLS 形式の動画を ExoPlayer + Kotlin で実装してみた

#はじめに
はじめまして！
都内で Android エンジニアとして働いています☺️
業務で動画プレイヤーを実装する機会があり、その際に Java + ExoPlayer で HLS形式の動画を表示するために実装したのですが、Kotlin では実装したことがなかったので調べたことを残すためにこの記事を書きました！

#ExoPlayerとは
 Androidの標準APIである、 `MediaPlayer` では対応していないアダプティブストリーミングなどに対応したオープンソースのライブラリです。
対応している形式は現時点(2019/09/17時点では)以下となります。

- HLS
- DASH
- SmoothStreaming

#実装
実装の手順に関しては、こちらの `ExoPlayer` の [Hello world!](https://exoplayer.dev/) を参考に進めていきます！
###依存関係を追加
まずは公式の手順に沿って、 Gradle に依存関係を追加していきます。
今回はHLSに対応するため、HLS用の依存関係も追加しています。

```gradle:build.gradle
    // ExoPlayerの必須ライブラリ
    implementation 'com.google.android.exoplayer:exoplayer-core:2.10.4'
    // ExoPlayer HLS対応
    implementation 'com.google.android.exoplayer:exoplayer-hls:2.10.4'
    // ExoPlayer UIを使用する場合
    implementation 'com.google.android.exoplayer:exoplayer-ui:2.10.4'
```

Java8 も有効にします。

```gradle:build.gradle
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
    }
```

###パーミッションを追加


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

###動画を表示するためのviewを作成

```xml:activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/playerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

今回は `ExoPlayer` の UIパッケージに存在する動画プレイヤー用の View を使用しています。
そして何も指定していいないのですが、 `PlayerView` には `app:resize_mode` という属性値があり、動画を縦・横・画面いっぱいなど、どこの大きさに合わせて表示するかなどを指定できるようになっています。
デフォルト値は fit になっており、他の指定できる設定としては以下となります。

- fit
- fixed_width
- fixed_height
- fill
- zoom


どのように動作するかについては[公式リファレンス](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.ResizeMode.html)の `AspectRatioFrameLayout.ResizeMode` のところに記載されています。

###Layout XML に配置した PlayerView と ExoPlayer のインスタンスと紐付けを行う

```kotlin:MainActivity.kt
    private lateinit var simpleExoPlayer: SimpleExoPlayer
    private lateinit var playerView: PlayerView

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

        // (1) 
        simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(this)
        simpleExoPlayer.apply {
            // builMediaSource() はのち説明しますが、
            // 形式に合わせた動画のデータを作っています。
            prepare(buildMediaSource())
        }
        
        playerView = findViewById(R.id.playerView)
        playerView.apply {
            // (2)
            player = simpleExoPlayer
        }
    }
```

(1) では `ExoPlayer` の `Factory` メソッドを使用して `ExoPlayer` のインスタンスを作成します。
このインスタンス生成時にバッファリングの際のコントロールなどの設定を渡してカスタマイズをすることができます。
ただ、今回はシンプルに画面にHLS形式の動画を表示することが目標なので `Context` を渡すのみの実装となります。

(2) 先ほど XML に配置した `PlayerView` と(1)で生成した `ExoPlayer` をの紐付けを行なっています

###DataSource と MediaSource を生成する
実際に動画を表示していくために表示する元となる動画のデータを作成していきます。

```kotlin
    private fun createDataSource(): DefaultDataSourceFactory {
        // (3) 
        return DefaultDataSourceFactory(
            this,
            getUserAgent(this, "作成したプロジェクト名")
        )
    }

    // 今回表示するサンプル動画
    val appleSample = 
            "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"

    private fun buildMediaSource(): HlsMediaSource {
        // (4)
        return HlsMediaSource
                .Factory(createDataSource())
                .createMediaSource(Uri.parse(appleSample))
    }
```

(3) ExoPlayer が動画などのメディアデータをやりとりするための DataSource を作成しています。

(4) ロードするメディアデータがどの形式なのかを ExoPlayer に伝えるために MediaSource インスタンスを作成しています。


上記の手順を踏むと、とりあえず以下のようにアプリの画面に動画が表示されるようになります。👏👏
<img width="318" alt="hlsスクショ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/1dc3cb88-7a90-589d-82e4-8bcbd0261a63.png">


###コード全体

```kotlin
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.ExoPlayerFactory
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util.getUserAgent

class MainActivity : AppCompatActivity() {
    private val appleSample = "http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8"

    private lateinit var playerView: PlayerView

    private lateinit var simpleExoPlayer: SimpleExoPlayer

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

        simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(this)
        simpleExoPlayer.apply {
            prepare(buildMediaSource())
        }

        playerView = findViewById(R.id.playerView)
        playerView.apply {
            player = simpleExoPlayer
        }
    }

    private fun createDataSource(): DefaultDataSourceFactory {
        return DefaultDataSourceFactory(
            this,
            getUserAgent(this, "作成したプロジェクト名")
        )
    }

    private fun buildMediaSource(): HlsMediaSource {
        return HlsMediaSource
            .Factory(createDataSource())
            .createMediaSource(Uri.parse(appleSample))
    }
}
```

#最後に
最初はとっつきにくく色々と苦戦した ExoPlayer でしたが、表示するだけであれば簡単に実装できるものだなぁと思いました。(ここからデザインや要件に合わせて色々とカスタマイズを行うと結構大変ですが。。。)
次は `PlayerView` や 今回は使用しなかった `PlayerControlView` などを使用してカスタマイズをどのように行なっていくかを書いていこうと思います。

見ていただきありがとうございます🙇‍♂️🙇‍♂️