ExoPlayerでHLSをData URIで復号・再生するには

※この記事は[DRM](https://ja.wikipedia.org/wiki/%E3%83%87%E3%82%B8%E3%82%BF%E3%83%AB%E8%91%97%E4%BD%9C%E6%A8%A9%E7%AE%A1%E7%90%86)が施されたHLSの復号についてではなく、[AES](https://ja.wikipedia.org/wiki/Advanced_Encryption_Standard)等で暗号化されたHLSを復号する方法について記載しています。


### HLSの暗号化の仕様

HLSストリームの暗号化に関する情報はm3u8ファイルの`#EXT-X-KEY`に記載されています。
例えば、以下のファイルでは暗号化の方法・暗号化キーのurl・[初期化ベクトル](https://ja.wikipedia.org/wiki/%E5%88%9D%E6%9C%9F%E5%8C%96%E3%83%99%E3%82%AF%E3%83%88%E3%83%AB)の値がわかります。

```
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:1653
#EXT-X-DISCONTINUITY-SEQUENCE:36
#EXT-X-KEY:METHOD=AES-128,URI="https://hogehoge",IV=0xdad51a9c61cc4344c03a566980ac21e7
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/a1qLvavaVwX.ts
#EXTINF:8.333334,
/ts/CD8xZLiy68pVp3/h264/480/LAAK1hXHVLa.ts
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/KXzsNPf8KYx.ts
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/JmHJgZGPZQA.ts
```

このキーファイルのURIは[Data URI scheme](https://ja.wikipedia.org/wiki/Data_URI_scheme)でもExoPlayerで再生することができます（[参考](https://github.com/google/ExoPlayer/issues/6312#issuecomment-522494022)）。
つまり、**暗号化キーのUrlではなくキーそのものをm3u8ファイルにベタ書きしてHLSを配信・再生することが可能**だということです。

### パーサをカスタマイズする
HLSに限らず、ExoPlayerではDataSourceを自由にinjectすることが可能です。
ここではデフォルトで使用されている[HlsPlaylistParser](https://github.com/google/ExoPlayer/blob/7d3f54a375fac04a746ca76a8f2e1ad32c8b45b2/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java)の代わりに自前でカスタマイズしたHLSMediaParserを使用してExoPlayerを初期化します。

``` kotlin
class CustomHlsPlaylistParserFactory() : HlsPlaylistParserFactory  {

    override fun createPlaylistParser(): ParsingLoadable.Parser<HlsPlaylist> =
        CustomHlsPlaylistParser()

    override fun createPlaylistParser(masterPlaylist: HlsMasterPlaylist): ParsingLoadable.Parser<HlsPlaylist> =
        CustomHlsPlaylistParser(masterPlaylist)
}

class CustomHlsPlaylistParser() : ParsingLoadable.Parser<HlsPlaylist> {

    
    @Throws(IOException::class)
    override parse(uri: Uri, inputStream: InputStream): HlsPlaylist {
     // パーサを実装
     // HlsPlaylistParser#parseMediaPlaylist`のfullSegmentEncryptionKeyUri`を任意のData Uri schemeに書き換えます
    }

    
    private fun toDataUriScheme(file: File): String {
        val base64str = Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)//NO_PADDINGは使用できません
        return "data:text/plain;base64,$base64str"
    }
}

```

```kotlin

fun initPlayer(remoteUrl: String): HlsMediaSource {
    val userAgent = WebView(getApplication()).settings.userAgentString
    val hppf = CustomHlsPlaylistParserWrapper(getApplication(), this)
    val dsf = DefaultDataSourceFactory(getApplication(), userAgent)//DefaultDataSourceFactory
    val mediaSource =  HlsMediaSource.Factory(dsf)
        .setPlaylistParserFactory(hppf)
        .createMediaSource(remoteUrl.toUri())
    player.prepare(mediaSource)
    player.playWhenReady = true
}
```

なお、[HLSの仕様書](https://tools.ietf.org/html/rfc8216#section-5.2)によればAES-128ではキーのパディングを省略できないようです。
> An encryption method of AES-128 signals that Media Segments are completely encrypted using the Advanced Encryption Standard (AES) [AES_128] with a 128-bit key, Cipher Block Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7) padding [RFC5652].  

また、DataSourceFactoryには[DefaultHttpDataSourceFactory](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.html)ではなく[DefaultDataSourceFactory](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.html)を用います（DRMを使用する場合は[DataSchemeDataSource.Factory](https://exoplayer.dev/doc/reference/index.html?com/google/android/exoplayer2/upstream/DataSchemeDataSource.html)）。もしDefaultHttpDataSourceFactoryを使用すると、キーのURIがUrlとして扱われてしまい、MalformedURLExceptionがスローされます。


これで任意のキーに書き換えることができました。