ExoPlayerはいつplaylistを更新するのか【HLS】【Android】

# はじめに
ExoPlayerとは、Androidで動画再生を行う際に使われる動画プレイヤーのライブラリです。今回はexoplayerでhlsを再生する際にplaylistが更新されるタイミングを調査したので、勉強の意味も込めてまとめてみます。

本記事はExoPlayerのバージョン2.6.0を元に書いています。
(ExoPlayerはライブラリの更新頻度が高い上に、毎回大きな変更が加わるため注意が必要です。)

# HLSとは
HLSとは、apple社が開発しているHTTPベースのストリーミングプロトコルで、Streaming配信を行う際に、よく使用されます。

## MasterPlaylistとMediaPlaylist
HLSはAdaptiveStreamingに対応しています。つまり、回線速度に応じて適切なBitrateを選択し、回線環境にあった動画を動的に再生することができます。このAdaptiveStreamingを実現するために、MasterPlaylistとMediaPlaylistという二種類のプレイリストが存在しています。

AdaptiveStreamingの詳しい説明は[こちら](https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming)を御覧ください。

<Adaptive Streamingの説明画像>

### MasterPlaylist
MasterPlaylistには各解像度ごとのPlaylistが示されています。再生時にはMasterPlaylistに示された適切な解像度のMediaPlaylistが選択されます。

```master.m3u8
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=184000
http://example.com/media_180.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000
http://example.com/media_240.m3u8

...

#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2400000
http://example.com/media_720.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5300000
http://example.com/media_1080.m3u8
```

### MediaPlaylist
MediaPlaylistには、特定の解像度の動画の一覧が記されています。プレイヤーはこのMediaPlaylistに記された動画ファイルを、ひとつづつダウンロードし動画を再生します。

```media_720.m3u8
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:3,
http://example.com/file1.ts
#EXTINF:10,
http://example.com/file2.ts
#EXTINF:4,
http://example.com/file3.ts
#EXTINF:7,
http://example.com/file4.ts
```

Playlistには#EXTから始まるタグをつけることができます。
例えば上記の#EXTINFはtsファイルの動画の長さを示しています。

#MediaPlaylistの更新

HLSでは生放送などのライブ配信も行うことができます。つまり、server側は動的にMediaPlaylistを更新する必要があります。

上記の`media_720.m3u8`を例に取ります。
プレイリスト内の`#EXT-X-MEDIA-SEQUENCE`はプレイリストの順番を示しており、この場合は1番目のプレイリストだということが分かります。

```media_720.m3u8
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:3,
http://example.com/file1.ts
#EXTINF:10,
http://example.com/file2.ts
#EXTINF:4,
http://example.com/file3.ts
#EXTINF:7,
http://example.com/file4.ts
```


再度プレイリストを取得しました。すると先ほどのプレイリストが更新されて以下のようになっていました。



```media_720.m3u8
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:2
#EXTINF:10,
http://example.com/file2.ts
#EXTINF:4,
http://example.com/file3.ts
#EXTINF:7,
http://example.com/file4.ts
#EXTINF:5,
http://example.com/file5.ts
```


`#EXT-X-MEDIA-SEQUENCE: 2`であることから、2番目のtsファイルを先頭にもつ新しいプレイリストが取得されたことが分かります。中のtsファイルもキチンと更新されています。


では、プレイリストはどの間隔で再取得するのが良いのでしょうか？
[HLSの仕様書](https://tools.ietf.org/html/rfc8216#section-6.3.1)を見てみると

> How the client obtains the URI to the Playlist file is outside the scope of this document

つまり、プレイリストの取得間隔は特に定められておらずプレイヤーの実装に委ねられているように思えます。

では、ExoPlayerはどのような間隔でPlaylistを再取得しているのでしょうか？
実際に実装を見ていきます。


ExoPlayerには、HLSのプレイリスト郡を扱う`HlsPlaylistTracker`クラスがあります。
（↓は必要な箇所だけ抜き出しています。）

```HlsPlaylistTracker.java

/**
 * Tracks playlists linked to a provided playlist url. The provided url might reference an HLS
 * master playlist or a media playlist.
 */
public final class HlsPlaylistTracker implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {

　　・・・

  /**
   * Holds all information related to a specific Media Playlist.
   */
  private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>>,

    private long earliestNextLoadTimeMs;

　　　　　　　　・・・

    public void loadPlaylist() {
      blacklistUntilMs = 0;
      if (loadPending || mediaPlaylistLoader.isLoading()) {
        // Load already pending or in progress. Do nothing.
        return;
      }
      long currentTimeMs = SystemClock.elapsedRealtime();
      if (currentTimeMs < earliestNextLoadTimeMs) {
        loadPending = true;
        playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);
      } else {
        loadPlaylistImmediately();
      }
    }

    private void processLoadedPlaylist(HlsMediaPlaylist loadedPlaylist) {
      HlsMediaPlaylist oldPlaylist = playlistSnapshot;
      long currentTimeMs = SystemClock.elapsedRealtime();
　　　　　　　　　　　　playlistSnapshot = getLatestPlaylistSnapshot(oldPlaylist, loadedPlaylist);
      ・・・

      // Do not allow the playlist to load again within the target duration if we obtained a new
      // snapshot, or half the target duration otherwise.
      earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist
          ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2));
      // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the
      // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes
      // the primary.
      if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) {
        loadPlaylist();
      }
    }

  }

・・・

}

```

`HlsPlaylistTracker`では、MasterPlaylistや各解像度のMediaPlaylistの情報を保持しています。そのInnerClassである`MediaPlaylistBundle`は選択されているMediaPalylistの情報を保持しています。

まず、`processLoadedPlaylist()`内の実装を見ていきます。このメソッドは、プレイリストが取得されたタイミングで呼ばれます。

この中に`playlistSnapshot.targetDurationUs`と言うものがあります。
targetDurationUsには、MediaPlaylistの`#EXT-X-TARGETDURATION`の値が保持されています。
`#EXT-X-TARGETDURATION`には、MediaPlaylist内のtsファイルの中で最も長い動画の縮尺の時間が示されています。
先程のMediaPlaylistの例では、`file2.ts`が10秒の長さであるため、`#EXT-X-TARGETDURATION`には10が入れられています。


`MediaPlaylistBundle`の`processLoadedPlaylist()`内で`earliestNextLoadTimeMs`を計算している箇所があります。保持しているPlaylistが新しければ`currentTimeMs+playlistSnapshot.targetDurationUs`、以前取得したものと同じものであれば`currentTimeMs+playlistSnapshot.targetDurationUs/2`が`earliestNextLoadTimeMs`に入れられています。

その後、MediaPlaylistにEndTagが含まれていなければ、`loadPlaylist()`が呼ばれるようになっており、この中の`playlistRefreshHandler.postDelayed(this, earliestNextLoadTimeMs - currentTimeMs);`または、`loadPlaylistImmediately();`で、MediaPlaylistの再取得が行われています。

## 結論
つまり、例外や特殊なケースもありますが、取得したMediaPlaylistが新しいものであれば、`#EXT-X-TARGETDURATION`秒後に、取得したMediaPlaylistが前回取得したMediaPlaylistと同じであれば、`#EXT-X-TARGETDURATION / 2`秒後に、MediaPlaylistが再取得されることになります。