Androidマンになりたいおじさん

自分の備忘録的なのです。Androidを普及できたらいいなと思ったりしながら書きます

ExoPlayeのABRはどう実装されてるか

はじめに

おはようこんにちはおやすみなさい!!!!!!!!!!最近、あつ森とゼノブレイドにハマってる、ぐみおじです!!!!!

今回はExoPlayerのABRについて、調べる機会があったので調べたのを記事にしました!

2020/04/10(金) 15:00時点のExoPlayerのversion2.11.4を対象としています

ABRとは

Adaptive bit rateの略

以下、日本デジタル・プロセシング・システムズ株式会社様のサイトより www.dpsj.co.jp

動画コンテンツを視聴環境(デバイスや回線速度など)に合わせて、可能な限り品質高くスムーズに再生できるようにするための動画配信技術

具体的には視聴端末の処理能力や解像度、回線速度などに応じて届ける動画の品質を自動的に上げたり下げたりすることで、視聴者に可能な限り快適な視聴体験を提供する

つまり、各ユーザーの視聴環境に合わせ、高品質な動画をスムーズに提供するための動画配信技術のこと

配信サーバーがABR対応のprotocolに準拠していたら、実現できます 主流なのはHTTPベースの以下の2つです

  • MPEG-DASH
  • HLS etc.

通信環境によって、適切な解像度の動画を提供する際に内部でどうやって見分けているか

ABRを実現するには、各帯域幅に対応した解像度を提供するmaster playlistをサーバーは用意して上げる必要があり、playerはそれらを内部で適切に選択する必要が出てくる

例: master playlist

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=1100000,RESOLUTION=852x480
http://example.com/480/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2200000,RESOLUTION=1280x720
http://example.com/720/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
http://example.com/1080/index.m3u8

各playerはこのBANDWIDTH(以下、帯域幅)の値を基準に内部で適切な動画を選択してるということになります

例えば、上のmaster playlistの定義を借りて、僕の一時的な帯域幅が3.2Mbpsだったとき、720のmedia playlistを選択して、再生しようとします

f:id:qurangumio:20200420184502j:plain
abr_image

参考動画

いくつかDroidkaigi2020でtakusembaさんが、共通する部分を動画で話してくださってるので動画で見たい人はこちらへ!!! youtu.be

でわ、ExoPlayerはどう実装しているかを見ていきましょう!!!

ExoPlayerのABR

結論

ユーザの帯域幅 >= master playlistに定義された帯域幅の関係が成り立つ、最も大きなplaylistを選び

現在の解像度より、上がる場合

  • bufferdが安全に切り替えれるくらいの最低限のbuffer(live edge込)を満たしている

現在の解像度より、下がる場合

  • bufferdが今すぐ切り替えないといけないくらいに余裕がない

簡易実装フロー

ここでは簡易的なフローを書いています

更に深ぼった実装詳細は下の方に書いています

初めにplayerがあるmater playlistをダウンロードして、再生を試みようとします

次にchunkをloadするタイミングがあり、そこで適切なmedia playlistを選択するロジックが入ります

ABR適応するときに以下の2つを考慮している

  • ユーザーの帯域幅はmaster playlistに定義された帯域幅を満たしてる?
  • 上記を満たしてるけど、安全に切り替えるために充分にbufferingできてる?

帯域幅について

playerはユーザの帯域幅を知る必要があり、player内で実測値を見積もる実装の必要があります

この見積もった値を使い、master playlistの帯域幅を大きい順から比較していき、それ以下を満たすmedia playlistが選択されます

bufferingについて

帯域幅比較で選ばれたmedia playlistを適応するかどうかの際に以下のparamsが関わってくる

  • 現在再生中のmediaの地点(current)
  • 現在load済みのmediaの地点(loaded)
  • buffer済みのmediaの期間(bufferd = loaded - current)
  • 現在edgeのmediaの地点(live edge)
補足: live edgeとは
playerはserverに動画をリクエストをし、server側で複雑な処理を重ね、最終的にレスポンスとして返ってきます
また、playerはbufferingしないと再生できないため、bufferingすることも含め、再生完了の状態になるまで、何秒かのlatencyが発生します
playerは一番最新のリクエストだけを知っているので、まだレスポンスとして返ってきてはいないが、何も問題がなければ、いずれ返ってくるのは分かっています
そのいずれ返ってくるはずであろう、一番最新のリクエストが返す動画コンテンツの秒数地点のことを指します
生放送や低遅延といった配信では、このlatencyの間隔が短くなるため、edgeが近いという表現を使います

f:id:qurangumio:20200420180004j:plain
buffering_params_image

更新するとき、以下の条件を満たしてると変わらない

  • 高品質に切り替えるくらいの帯域幅はあるけど、安全に切り替えるためのminBuffer(指定可)がbufferedより大きい(minBuffer > buffered)
    • もしくは、Live streamingの場合はedgeが近くなるため、minBufferを満たせないという状況が増え、代わりにedgeを基準にして、currentからedgeまでを全体とし、bufferedが何割(指定可)以下しか占めていない
  • 低品質に切り替えないといけない帯域幅だけど、bufferdが今すぐ切り替える必要がないくらい余裕(maxBuffer)があるとき(指定可)

僕の帯域幅が3.2Mbpsあり、720のmedia playlistを再生していたとします 通信環境が少し悪くなり、1.6Mbpsほどの帯域幅になりました

順番にmaster playlistの帯域幅と比較した結果、480のmedia playlistが取得できることになりました

  • 1080: 5.0Mbps >= 1.6Mbps -> x
  • 720: 2.2Mbps >= 1.6Mbps -> x
  • 480: 1.1Mbps <= 1.6Mbps -> o

720から480の低品質に切り替える必要がありますが、この段階で、bufferingはどれほどたまってるでしょう?

  • current(現在の再生地点): 45s
  • loaded(現在のload済地点): 55s
  • buffered(現在buffer済みの期間): 10s
  • live edge(現在判明しているedgeの地点): 60s

今すぐ切り替える必要がないくらいのbuffer(maxBufferとする)を25s間と設定していたとします(指定可) bufferd < maxBuffer -> x

bufferに余裕がない判定となり、480に切り替わることが確定しました

実装詳細

ここからは、ExoPlayerのABR実装がどのように実現しているかをみていきます

Keyとなってくるのは以下の3つのclassです

  • AdaptiveTrackSelection
  • DefaultBandwidthMeter
  • DefaultBandwidthProvider

簡易フローより

次にchunkをloadするタイミングがあり、そこで適切なmedia playlistを選択するロジックが入ります

これはHlsChunkSource#getNextChunk()内のAdaptiveTrackSelection#updateSelectedTrack()を呼び出しています

AdaptiveTrackSelection

ユーザの帯域幅を推測した結果を用いて、master playlistの帯域幅と比較し、解像度を上げるか、下げるかを内部で判断しているclassです

これはDefaultTrackSelector生成時に constructor内でdefaultで作られます

また、ExoPlayerがDefaultですぐに利用できるように用意してあるだけで、自分でカスタマイズしたTrackSelectionを指定することも可能です

このクラスでABRを実現するために大事なParamsが以下のようにあります

@Nullable private final BandwidthMeter bandwidthMeter;
 -> ユーザの帯域幅を計測して、提供してくれる

private final int minDurationForQualityIncreaseMs; 
 -> default: 10000ms
 -> 高品質に切り替わるのに必要なbufferの最小期間

private final int maxDurationForQualityDecreaseMs;
 -> default: 25000ms
 -> 低品質に切り替わるのに必要なbufferの最大期間

private final float bandwidthFraction;
 -> default: 0.7f
 -> 実計測値の内、使用可能とみなす帯域幅の割合。Estimatorの不正確さを考慮して、1以下がいいらしい

private final float bufferedFractionToLiveEdgeForQualityIncrease;
 -> default: 0.75f
 -> Live streamingの場合はedgeが近くなるため、minBufferを満たせないという状況が増え、代わりにedgeを基準にして、currentからedgeまでを全体とし、bufferedが占めている割合

生成段階の説明は今回の主旨とズレるので、割愛します

HlsChunkSource#getNextChunk()から、updateTrackSelectionsが呼ばれた際の説明をしていきます

やってることとしては、media playlistsが配列表現されており、TrackSelectionは選択したplaylistのselectedIndexとしてフィールドを持ってるので、そのindexを更新してる処理をdetermineIdealSelectedIndexで行ってます

ここで、DefaultBandwidthProviderから帯域幅(以下、effectiveBitrate)をもらい、master playlistの帯域幅がそれ以下だと適用されます

更に言うと、再生速度(playbackSpeed)に邪魔されないようにmaster playlistの帯域幅にplaybackSpeedを掛けた上で比較します

Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate;

この時点で、ユーザに次に返すコンテンツ候補が選ばれた状態です

まだ、候補なだけで適用ではないです

最初の結論のとこにも書いたとおり、条件を満たした場合は解像度が現状維持のままで変わりません

if (selectedFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) {
  selectedIndex = currentSelectedIndex;
} else if (selectedFormat.bitrate < currentFormat.bitrate && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
  selectedIndex = currentSelectedIndex;
}

minDurationForQualityIncreaseUs(availableDurationUs)は、live edge込でみてくれてます

これで最終的に適用されるselectedIndexがHlsChunkSourceにいき、Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex];という形でとってきてくれるわけです

ちなみに内部で、bitrate比較は高い順にみていってるので、最初にtrueとなったindexが選ばれるようになってます

これは親のBaseTrackSeletionの内部でcompareをoverrideしていて、compare(b.bitrate - a.bitrate)になっていることから、高い順に調べてることがわかる

DefaultBandwidthMeter

BandwidthMeterをDefault実装していて、帯域幅を計測してくれるclassです

以下のparamが計測する際に大きく関与します(指定可)

private int slidingWindowMaxWeight;
 -> default: 2000
 -> playerが帯域幅を必要としたタイミング時に、合計でどのくらい過去のサンプリングまでを参考にするかの重みの最高値

ユーザがmedia fileをダウンロードしてくる際にどのくらい時間がかかったかのサンプリングを集めていて、帯域幅を必要とされた時から、一定の過去の区間までの中央値を計測値としています

この過去の区間で、どこまで遡るかを決めるのがslidingWindowMaxWeight(以下maxWeight)で、これはサンプル1つ1つにweightがつけられていて、このweightがtotalを超えるまでを中央値の区間とされます(totalWeight > maxWeight)

f:id:qurangumio:20200420180100j:plain
bandwidthの計測

また、初期bitrate(指定可)は計測されていないので、分かりません

そのため、ExoPlayerは各network, 、国別で予めグループ分けして、決め打ちで定義がされています

色んな国のnetwork事情が見えてくるので、一度覗いてみると面白いと思います

  /** Default initial Wifi bitrate estimate in bits per second. */
  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI =
      new long[] {5_700_000, 3_500_000, 2_000_000, 1_100_000, 470_000};

  /** Default initial 2G bitrate estimates in bits per second. */
  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_2G =
      new long[] {200_000, 148_000, 132_000, 115_000, 95_000};

  /** Default initial 3G bitrate estimates in bits per second. */
  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_3G =
      new long[] {2_200_000, 1_300_000, 970_000, 810_000, 490_000};

  /** Default initial 4G bitrate estimates in bits per second. */
  public static final long[] DEFAULT_INITIAL_BITRATE_ESTIMATES_4G =
      new long[] {5_300_000, 3_200_000, 2_000_000, 1_400_000, 690_000};

日本では以下のように定義されています

ex.) {WIFI, 2G, 3G, 4G}
countryGroupAssignment.put("JP", new int[] {0, 2, 1, 1});

これを各配列に当てはめてみると、日本の初期値はこんな感じになりそうです

{WIFI: 5_700_000, 2G: 132_000, 3G: 1_300_000, 4G: 3_200_000}

ちなみに5Gは現段階では、WIFIと同じになってます この初期値は最初のmedia playlist取得の際に利用されます

いやいや、日本のWIFIが全員5.7Mbpsもあると思ったら、大間違いですよ...ってなりますよね

そのためにsetInitialBitrateEstimate(long initialBitrateEstimate)があり、これで初期bitrateを指定してあげることができます

おまけ程度ですが、setResetOnNetworkTypeChangeをONにすると、networkTypeが変わるたびに初期値に戻ります

DefaultBandwidthProvider

BandwidthProvider interfaceをDefault実装していて、割り当てた帯域幅を提供してるclassです

上記のBandwidthMeterにより、計測された帯域幅にbandwidthFraction(指定可)をかけた帯域幅が、最終的にmaster playlistの帯域幅と比較されます

これはインターネットの不確実性を考慮して、実際の値より小さめに設定するために以下のような計算が行われています

bandwidthMeter.getBitrateEstimate() * bandwidthFraction

さいごに

動画技術って一見難しそうですが、やってることはシンプルなこと多いですよね(たぶん)

こういうのをもっと知っていけたらなと思います!!!これからも学びの気持ちでいきましょう!!!

それでは皆さん、よいExo lifeを!!!!!!