Android
HLS
Exoplayer
playlist

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の詳しい説明はこちらを御覧ください。

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ファイルもキチンと更新されています。

では、プレイリストはどの間隔で再取得するのが良いのでしょうか?
HLSの仕様書を見てみると

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が入れられています。

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

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

結論

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