ExoPlayerのアダプティブストリーミングの実装を覗いてみる

ExoPlayerはMPEG DASHなどのアダプティブストリーミングプロトコルに対応しているのですが、実際にどのようにして複数のストリームから最適なものを選んでいるのかを調べてみました。
ストリーム(トラック)を選択しているのは`TrackSelection`インターフェースを実装している`AdaptiveTrackSelection`クラスのようです(実際にはいくつかの親クラスが間にいますが)。

```java
@Override
public void updateSelectedTrack(long bufferedDurationUs) {
  // 現在時刻
  long nowMs = SystemClock.elapsedRealtime();

  // 現在のトラックインデックスとフォーマット
  int currentSelectedIndex = selectedIndex;
  Format currentFormat = getSelectedFormat();

  // 理想的なトラックインデックスとフォーマット
  int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
  Format idealFormat = getFormat(idealSelectedIndex);

  // 理想的なトラックに切り替える
  selectedIndex = idealSelectedIndex;

  // もし切り替えない方がいい状態なら、切り替えを元に戻す
  // 現在再生しているフォーマットがあり、選択されたトラックがブラックリストになっていない
  if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) {

    if (idealFormat.bitrate > currentFormat.bitrate
        && bufferedDurationUs < minDurationForQualityIncreaseUs) {
      // 理想的なトラックの方が高品質だが、安全にストリームを切り替えるためには十分なバッファがない
      selectedIndex = currentSelectedIndex;
    } else if (idealFormat.bitrate < currentFormat.bitrate
        && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
      // 理想的なトラックの方が低品質だが、今すぐに切り替えなくても良いくらいのバッファがある
      selectedIndex = currentSelectedIndex;
    }
  }

  // もしトラックが変わったなら、トリガーを実行する
  if (selectedIndex != currentSelectedIndex) {
    reason = C.SELECTION_REASON_ADAPTIVE;
  }
}
```

上記コードの中で「理想的なトラック」を`determineIdealSelectedIndex`で選んでいましたが、この中身はどうなっているのでしょうか。

```java
private int determineIdealSelectedIndex(long nowMs) {

  // 通信帯域からビットレートを取得
  long bitrateEstimate = bandwidthMeter.getBitrateEstimate();

  // 適切なビットレートを計算
  // 通信帯域からビットレートが取得できない場合は、maxInitialBitrateで指定した値。
  // 通信帯域からビットレートが取得できた場合は、その値にbandwidthFractionをかけた値。
  long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE
      ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);

  // ブラックリストになっていない最も低いインデックス(=高いビットレート)
  int lowestBitrateNonBlacklistedIndex = 0;

  // 全トラックをビットレートの高い順に調べる
  for (int i = 0; i < length; i++) { 
    // 該当トラックがブラックリストになっていないかつ
    // 該当トラックのビットレートが上で計算した適切なビットレートより小さければ
    // それを返す
    if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
      Format format = getFormat(i);
      if (format.bitrate <= effectiveBitrate) {
        return i;
      } else {
        lowestBitrateNonBlacklistedIndex = i;
      }
    }
  }

  // 適切なビットレートより大きい中で最もビットレートの低い値が設定されているので、それを返す
  return lowestBitrateNonBlacklistedIndex;
}
```

ここでなぜループを回している時にビットレートが高い順に調べていることがわかるかというと、`AdaptiveTrackSelection`の親クラスである`BaseTrackSelection`のコンストラクタの中でビットレートの高い順に並べ替えている箇所があるからです。

```java
Arrays.sort(formats, new DecreasingBandwidthComparator());
...

private static final class DecreasingBandwidthComparator implements Comparator<Format> {
  @Override
  public int compare(Format a, Format b) {
    return b.bitrate - a.bitrate;
  }
}
```

回線状況が不明な場合に最初から高いビットレートで再生を試みたいと思ったら、コンストラクタの`maxInitialBitrate`をいい感じにすればいいのかな？
回線状況が一瞬悪くなった時にすぐビットレートが下がるのを避けたかったら`maxDurationForQualityDecreaseMs`を大きめにすればいいのかな？(回線状況が悪いままだと再生が止まりますが)
逆に回線状況が良くなった時にすぐビットレートを上げたければ`minDurationToRetainAfterDiscardMs`を小さくすればいいのかな？(低ビットレートのストリームで蓄えたバッファが無駄になる可能性がありますが)
可能な限り通信帯域をめいいっぱい使ってビットレートを上げたかったら`bandwidthFraction`を高めにすれば良さそうだけど、1以上にするとたぶん無理。デフォルトは`0.75`。