LoginSignup
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-22

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

@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で選んでいましたが、この中身はどうなっているのでしょうか。

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のコンストラクタの中でビットレートの高い順に並べ替えている箇所があるからです。

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7