ExoPlayerは通信状況に応じて適切なビットレートのメディアを読み込むアダプティブストリーミングに対応しています。「通信状況に応じて」というのは具体的にはサーバーからどれくらいの速度でデータを読み込めるかを測定することなのですが、これを行うためのBandwidthMeter
というインターフェイスが用意されています。このインターフェイスをExoPlayerのインスタンスを作るときに渡して上げると、メディアの読み込みバイト数や時間をコールバックで受け取ることができ、そこから通信回線の速度を計算することができます。
通信速度を計算する部分の処理を自分で書くこともできますが、デフォルトの実装としてDefaultBandwidthMeter
が用意されています。通常はこれを使うことになるでしょう。
では、このDefaultBandwidthMeter
は具体的にどうやって通信速度の計算をするのでしょうか?ソースコードを覗いてみました。
DefaultBandwidthMeterはBandwidthMeter, TransferListenerという二つのインターフェイスを実装します。これはつまり、BandwidthMeterが必要な箇所とTransferListenerが必要な箇所それぞれにこのインスタンスを渡す必要があることを意味します。
最初このことを知らずにAdaptiveTrackSelection.Factory
のコンストラクターにDefaultBandwidthMeter
を渡しておけば十分かと思っていたのですが、それは間違いでした。DefaultDataSourceFactory
のコンストラクターにも同じインスタンスを渡さなければいけません。
詳しくはこちらの記事を参照ください。
DefaultDataSourceFactory
のコンストラクタにDefaultBandwidthMeter
を渡すとメディアデータを受信開始した時、受信した時、受信を終えた時にそれぞれDefaultBandwidthMeter
内のコールバックが呼ばれるようになります。このコールバック内でデータの受信量と受信にかかった時間から通信速度を計算しています。計算している部分は以下のようになっています(一部抜粋)。
// データ受信開始時に呼ばれる
@Override
public synchronized void onTransferStart(Object source, DataSpec dataSpec) {
// 複数回呼ばれることがあるので、最初に呼ばれた時だけ現在時刻をフィールドに保持する
if (streamCount == 0) {
sampleStartTimeMs = SystemClock.elapsedRealtime();
}
// 呼ばれた回数をカウントアップする
streamCount++;
}
// データ受信した時に呼ばれる
@Override
public synchronized void onBytesTransferred(Object source, int bytes) {
// これまでに受信したデータ量を合計する※1
sampleBytesTransferred += bytes;
}
// 受信完了した時に呼ばれる
@Override
public synchronized void onTransferEnd(Object source) {
Assertions.checkState(streamCount > 0);
// 現在時刻
long nowMs = SystemClock.elapsedRealtime();
// 受信開始してから現在までの時間
int sampleElapsedTimeMs = (int) (nowMs - sampleStartTimeMs);
// 受信にかかった合計時間
totalElapsedTimeMs += sampleElapsedTimeMs;
// 受信した合計データ量
totalBytesTransferred += sampleBytesTransferred;
if (sampleElapsedTimeMs > 0) {
// 受信データ量 / 受信時間 = 1秒あたりの受信データ量(bps)
float bitsPerSecond = (sampleBytesTransferred * 8000) / sampleElapsedTimeMs;
// ログを追加※2
slidingPercentile.addSample((int) Math.sqrt(sampleBytesTransferred), bitsPerSecond);
// 合計受信時間が一定以上あるか、合計受信量が一定以上ある場合※3
if (totalElapsedTimeMs >= ELAPSED_MILLIS_FOR_ESTIMATE
|| totalBytesTransferred >= BYTES_TRANSFERRED_FOR_ESTIMATE) {
// ログの中央値を取得※2
float bitrateEstimateFloat = slidingPercentile.getPercentile(0.5f);
// 中央値が有効な値なら、それを通信速度として保持する
bitrateEstimate = Float.isNaN(bitrateEstimateFloat) ? NO_ESTIMATE
: (long) bitrateEstimateFloat;
}
}
// ストリーム数を減らす。まだ残りストリームがある場合は、測定開始時間を現時刻に設定する
if (--streamCount > 0) {
sampleStartTimeMs = nowMs;
}
// 測定データ量をリセット
sampleBytesTransferred = 0;
}
※1 sampleBytesTransferred は onTransferEnd が呼ばれるたびに0にリセットされます。
※2 slidingPercentile.addSample()
,slidingPercentile.getPercentile()
の実装については割愛しますが、パーセンタイル0.5は中央値のことであるということさえ理解できれば十分です。
中央値とはデータ列を昇順に並べ替えた時に真ん中にある値(偶数個のデータの場合はちょうど真ん中がないので、真ん中にある二つのデータの平均)のことです。平均値よりも中央値の方が適切であるという判断なのでしょう。
※3 合計受信量または合計受信時間が一定値に達するまで通信速度の計算は行われません。この閾値はそれぞれ定数で2秒または512KBと定義されています。つまり、2秒経過するか512KBダウンロードするまでの間はgetBitrateEstimate()
は推定された通信速度を返しません。この場合、NO_ESTIMATE(-1)
が返されます。