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
。