ExoPlayer供養

# ごあいさつ
この記事は[Globeeアドベントカレンダー](https://qiita.com/advent-calendar/2023/globee)2日目の記事です。

この度はExoPlayer供養にご列席下さりありがとうございます。
ExoPlayerがDeprecatedになったことで、出す機会を失ったナレッジを記事化し、ExoPlayerの供養としたいと思います。

# この記事で書きたいこと
* ExoPlayerを使用して、俺俺HLSのマニフェストをパースして再生する
* ExoPlayerを使用して、DRMコンテンツを再生する
* 複数のフォーマットのコンテンツ保護を共存させる

# この記事でやらないこと
* コンテンツ保護の意義や解説
* 一般的な使い方

# ExoPlayerで俺俺HLSのマニフェストをパースする
HLSはDRMに比べると弱いと言われていますが、逆を言えば融通が聞くということでもあります。
マニフェストファイルを独自フォーマットに変更すれば鍵の取得など、コンテンツ管理に融通を利かすことができます。

## 1. パサーを実装する
HLSのPlayListは [HlsPlaylistParser](https://github.com/google/ExoPlayer/blob/r2.15.1/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java) クラスでパースされます。
このクラスをコピーして独自パサーを実装しましょう。
このコピーして改変したクラスを `MyHlsPlaylistParser` とします。

```kotlin:MyHlsPlaylistParser
class MyHlsPlaylistParser(
    private val masterPlaylist: HlsMasterPlaylist,
    private val previousMediaPlaylist: HlsMediaPlaylist?,
) : ParsingLoadable.Parser<HlsPlaylist> {
    // パサーの変更内容は省略
}
```

## 2. パサーのFactoryを実装する
`HlsPlaylistParserFactory` クラスを継承した `MyHlsPlaylistParserFactory` クラスを実装します。
(1.)で作った`MyHlsPlaylistParser` は `ParsingLoadable.Parser<HlsPlaylist>` を継承していますので、以下のままで大丈夫です。
```kotlin:MyHlsPlaylistParserFactory
class MyHlsPlaylistParserFactory : HlsPlaylistParserFactory {
    override fun createPlaylistParser(): ParsingLoadable.Parser<HlsPlaylist> =
        MyHlsPlaylistParser()
    override fun createPlaylistParser(
        masterPlaylist: HlsMasterPlaylist,
        previousMediaPlaylist: HlsMediaPlaylist?
    ): ParsingLoadable.Parser<HlsPlaylist> =
        MyHlsPlaylistParser(masterPlaylist, previousMediaPlaylist)
}
```

## 3. MediaSource.Factory にパサーを設定しよう
hlsのMediaSource.Factoryは `HlsMediaSource.Factory` を使用します。
```kotlin:
val hlsMediaSourceFactory = HlsMediaSource.Factory(dataSourceFactory)
                .setPlaylistParserFactory(MyHlsPlaylistParserFactory())
```

MediaSource.Factoryまで作成できれば、後はExoPlayerインスタンス作成時に設定できるはずです。

# ExoPlayerでDrm
DRMはHLSに比べると、ライセンスサーバで認証を得る必要があり、継続的な費用が発生しますが、強固にコンテンツを保護できます。
著作権のあるコンテンツを配信する場合必須級になってくるため、ExoPlayer側でも手厚くサポートされており標準の機能で実装できます。
ExoではMPEG-DASHをサポートしておりMediaItem生成時に指定することができます。
今回の例では、WideVineを使用してMediaItemを生成します。

```kotlin
/**
 * setUri: MPEG-DASHマニフェスト.mpd
 * setDrmUuid: `com.google.android.exoplayer2.C.WIDEVINE_UUID` 固定
 * setDrmLicenseUri: ライセンスファイル場所。通常Appサーバ
 * setDrmLicenseRequestHeaders: DRMライセンス取得にAuthorizationヘッダが必要な場合などはこれを使用する
 */
MediaItem.Builder()
        .setUri("https://localhost/path/to/video-content.mpd")
        .setDrmUuid(com.google.android.exoplayer2.C.WIDEVINE_UUID)
        .setDrmLicenseUri("https:localhost/path/to/wide-vine/licence/file")
        .setDrmLicenseRequestHeaders("Authorization Header" to "Header-Value")
        .build()
```

生成したメディアアイテムを使用してコンテンツの再生を行うと、後はExoPlayerが決まったフォーマットでAppサーバとやり取りしてくれます。

# 様々なフォーマットのメディアソースをいい感じに使いたい
上の2例で俺俺HLSとDRMで保護されたコンテンツを再生する方法を紹介しましたが、1点まだ問題があります。

様々なメディアタイプまぜこぜに再生したいと思ったとき、
メディアタイプ毎にを適切なMediaSource.Factoryに振り分ける機能はExoPlayer側にあるため、
このままでは俺俺HLSと他のフォーマットを共存させることができません。

これらを共存させるためには、メディアタイプ毎に適切なMediaSource.Factoryに振り分ける、親MediaSource.Factoryを自作する必要があります。
通常この機能は [DefaultMediaSourceFactory](https://github.com/google/ExoPlayer/blob/r2.15.1/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java) によって提供されます。

なので、 `DefaultMediaSourceFactory` も `HlsPlaylistParser` のケースと同じく、コピーして改変しましょう。
このコピーしてきたクラスを `MyMediaSourceFactory` とします。

```kotlin:MyMediaSourceFactory:384行付近
        try {
            // 変更点: 俺俺HLSに対応させるためDefaultMediaSourceFactoryから変更
//            val factoryClazz =
//                Class.forName("com.google.android.exoplayer2.source.hls.HlsMediaSource\$Factory")
//                    .asSubclass(MediaSourceFactory::class.java)
//            factories.put(
//                C.TYPE_HLS,
//                factoryClazz.getConstructor(DataSource.Factory::class.java)
//                    .newInstance(dataSourceFactory)
//            )
            val hlsMediaSourceFactory = HlsMediaSource.Factory(dataSourceFactory)
                .setPlaylistParserFactory(MyHlsPlaylistParserFactory())
            factories.put(C.TYPE_HLS, hlsMediaSourceFactory)
        } catch (e: Exception) {
            // Expected if the app was built without the hls module.
        }
```
HLSのMediaSourceFactoryを自作したメディアソースに書き換えます。

この改変した `MyMediaSourceFactory` を通常のExoPlayerの生成時に指定することで、適切にコンテンツ振り分けを行うMediaSourceFactoryになります。

# おわり
いかがでしたでしょうか？
Media3になってもこの知見が生かされることを信じて、ExoPlayer供養の結びとしたいと思います。