summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
blob: 33f8606e9b9b10e229c5ba783506858971b61765 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection;

import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
import org.mozilla.thirdparty.com.google.android.exoplayer2.SimpleExoPlayer;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroup;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunk;
import org.mozilla.thirdparty.com.google.android.exoplayer2.source.chunk.MediaChunkIterator;
import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;

/**
 * A bandwidth based adaptive {@link TrackSelection}, whose selected track is updated to be the one
 * of highest quality given the current network conditions and the state of the buffer.
 */
public class AdaptiveTrackSelection extends BaseTrackSelection {

  /** Factory for {@link AdaptiveTrackSelection} instances. */
  public static class Factory implements TrackSelection.Factory {

    @Nullable private final BandwidthMeter bandwidthMeter;
    private final int minDurationForQualityIncreaseMs;
    private final int maxDurationForQualityDecreaseMs;
    private final int minDurationToRetainAfterDiscardMs;
    private final float bandwidthFraction;
    private final float bufferedFractionToLiveEdgeForQualityIncrease;
    private final long minTimeBetweenBufferReevaluationMs;
    private final Clock clock;

    /** Creates an adaptive track selection factory with default parameters. */
    public Factory() {
      this(
          DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
          DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
          DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
          DEFAULT_BANDWIDTH_FRACTION,
          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
          Clock.DEFAULT);
    }

    /**
     * @deprecated Use {@link #Factory()} instead. Custom bandwidth meter should be directly passed
     *     to the player in {@link SimpleExoPlayer.Builder}.
     */
    @Deprecated
    @SuppressWarnings("deprecation")
    public Factory(BandwidthMeter bandwidthMeter) {
      this(
          bandwidthMeter,
          DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
          DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
          DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
          DEFAULT_BANDWIDTH_FRACTION,
          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
          Clock.DEFAULT);
    }

    /**
     * Creates an adaptive track selection factory.
     *
     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
     *     selected track to switch to one of higher quality.
     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
     *     selected track to switch to one of lower quality.
     * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
     *     quality, the selection may indicate that media already buffered at the lower quality can
     *     be discarded to speed up the switch. This is the minimum duration of media that must be
     *     retained at the lower quality.
     * @param bandwidthFraction The fraction of the available bandwidth that the selection should
     *     consider available for use. Setting to a value less than 1 is recommended to account for
     *     inaccuracies in the bandwidth estimator.
     */
    public Factory(
        int minDurationForQualityIncreaseMs,
        int maxDurationForQualityDecreaseMs,
        int minDurationToRetainAfterDiscardMs,
        float bandwidthFraction) {
      this(
          minDurationForQualityIncreaseMs,
          maxDurationForQualityDecreaseMs,
          minDurationToRetainAfterDiscardMs,
          bandwidthFraction,
          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
          Clock.DEFAULT);
    }

    /**
     * @deprecated Use {@link #Factory(int, int, int, float)} instead. Custom bandwidth meter should
     *     be directly passed to the player in {@link SimpleExoPlayer.Builder}.
     */
    @Deprecated
    @SuppressWarnings("deprecation")
    public Factory(
        BandwidthMeter bandwidthMeter,
        int minDurationForQualityIncreaseMs,
        int maxDurationForQualityDecreaseMs,
        int minDurationToRetainAfterDiscardMs,
        float bandwidthFraction) {
      this(
          bandwidthMeter,
          minDurationForQualityIncreaseMs,
          maxDurationForQualityDecreaseMs,
          minDurationToRetainAfterDiscardMs,
          bandwidthFraction,
          DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
          DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
          Clock.DEFAULT);
    }

    /**
     * Creates an adaptive track selection factory.
     *
     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
     *     selected track to switch to one of higher quality.
     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
     *     selected track to switch to one of lower quality.
     * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
     *     quality, the selection may indicate that media already buffered at the lower quality can
     *     be discarded to speed up the switch. This is the minimum duration of media that must be
     *     retained at the lower quality.
     * @param bandwidthFraction The fraction of the available bandwidth that the selection should
     *     consider available for use. Setting to a value less than 1 is recommended to account for
     *     inaccuracies in the bandwidth estimator.
     * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
     *     duration from current playback position to the live edge that has to be buffered before
     *     the selected track can be switched to one of higher quality. This parameter is only
     *     applied when the playback position is closer to the live edge than {@code
     *     minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
     *     quality from happening.
     * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
     *     buffer and discard some chunks of lower quality to improve the playback quality if
     *     network conditions have changed. This is the minimum duration between 2 consecutive
     *     buffer reevaluation calls.
     * @param clock A {@link Clock}.
     */
    @SuppressWarnings("deprecation")
    public Factory(
        int minDurationForQualityIncreaseMs,
        int maxDurationForQualityDecreaseMs,
        int minDurationToRetainAfterDiscardMs,
        float bandwidthFraction,
        float bufferedFractionToLiveEdgeForQualityIncrease,
        long minTimeBetweenBufferReevaluationMs,
        Clock clock) {
      this(
          /* bandwidthMeter= */ null,
          minDurationForQualityIncreaseMs,
          maxDurationForQualityDecreaseMs,
          minDurationToRetainAfterDiscardMs,
          bandwidthFraction,
          bufferedFractionToLiveEdgeForQualityIncrease,
          minTimeBetweenBufferReevaluationMs,
          clock);
    }

    /**
     * @deprecated Use {@link #Factory(int, int, int, float, float, long, Clock)} instead. Custom
     *     bandwidth meter should be directly passed to the player in {@link
     *     SimpleExoPlayer.Builder}.
     */
    @Deprecated
    public Factory(
        @Nullable BandwidthMeter bandwidthMeter,
        int minDurationForQualityIncreaseMs,
        int maxDurationForQualityDecreaseMs,
        int minDurationToRetainAfterDiscardMs,
        float bandwidthFraction,
        float bufferedFractionToLiveEdgeForQualityIncrease,
        long minTimeBetweenBufferReevaluationMs,
        Clock clock) {
      this.bandwidthMeter = bandwidthMeter;
      this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
      this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs;
      this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs;
      this.bandwidthFraction = bandwidthFraction;
      this.bufferedFractionToLiveEdgeForQualityIncrease =
          bufferedFractionToLiveEdgeForQualityIncrease;
      this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
      this.clock = clock;
    }

    @Override
    public final @NullableType TrackSelection[] createTrackSelections(
        @NullableType Definition[] definitions, BandwidthMeter bandwidthMeter) {
      if (this.bandwidthMeter != null) {
        bandwidthMeter = this.bandwidthMeter;
      }
      TrackSelection[] selections = new TrackSelection[definitions.length];
      int totalFixedBandwidth = 0;
      for (int i = 0; i < definitions.length; i++) {
        Definition definition = definitions[i];
        if (definition != null && definition.tracks.length == 1) {
          // Make fixed selections first to know their total bandwidth.
          selections[i] =
              new FixedTrackSelection(
                  definition.group, definition.tracks[0], definition.reason, definition.data);
          int trackBitrate = definition.group.getFormat(definition.tracks[0]).bitrate;
          if (trackBitrate != Format.NO_VALUE) {
            totalFixedBandwidth += trackBitrate;
          }
        }
      }
      List<AdaptiveTrackSelection> adaptiveSelections = new ArrayList<>();
      for (int i = 0; i < definitions.length; i++) {
        Definition definition = definitions[i];
        if (definition != null && definition.tracks.length > 1) {
          AdaptiveTrackSelection adaptiveSelection =
              createAdaptiveTrackSelection(
                  definition.group, bandwidthMeter, definition.tracks, totalFixedBandwidth);
          adaptiveSelections.add(adaptiveSelection);
          selections[i] = adaptiveSelection;
        }
      }
      if (adaptiveSelections.size() > 1) {
        long[][] adaptiveTrackBitrates = new long[adaptiveSelections.size()][];
        for (int i = 0; i < adaptiveSelections.size(); i++) {
          AdaptiveTrackSelection adaptiveSelection = adaptiveSelections.get(i);
          adaptiveTrackBitrates[i] = new long[adaptiveSelection.length()];
          for (int j = 0; j < adaptiveSelection.length(); j++) {
            adaptiveTrackBitrates[i][j] =
                adaptiveSelection.getFormat(adaptiveSelection.length() - j - 1).bitrate;
          }
        }
        long[][][] bandwidthCheckpoints = getAllocationCheckpoints(adaptiveTrackBitrates);
        for (int i = 0; i < adaptiveSelections.size(); i++) {
          adaptiveSelections
              .get(i)
              .experimental_setBandwidthAllocationCheckpoints(bandwidthCheckpoints[i]);
        }
      }
      return selections;
    }

    /**
     * Creates a single adaptive selection for the given group, bandwidth meter and tracks.
     *
     * @param group The {@link TrackGroup}.
     * @param bandwidthMeter A {@link BandwidthMeter} which can be used to select tracks.
     * @param tracks The indices of the selected tracks in the track group.
     * @param totalFixedTrackBandwidth The total bandwidth used by all non-adaptive tracks, in bits
     *     per second.
     * @return An {@link AdaptiveTrackSelection} for the specified tracks.
     */
    protected AdaptiveTrackSelection createAdaptiveTrackSelection(
        TrackGroup group,
        BandwidthMeter bandwidthMeter,
        int[] tracks,
        int totalFixedTrackBandwidth) {
      return new AdaptiveTrackSelection(
          group,
          tracks,
          new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, totalFixedTrackBandwidth),
          minDurationForQualityIncreaseMs,
          maxDurationForQualityDecreaseMs,
          minDurationToRetainAfterDiscardMs,
          bufferedFractionToLiveEdgeForQualityIncrease,
          minTimeBetweenBufferReevaluationMs,
          clock);
    }
  }

  public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
  public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
  public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
  public static final float DEFAULT_BANDWIDTH_FRACTION = 0.7f;
  public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
  public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;

  private final BandwidthProvider bandwidthProvider;
  private final long minDurationForQualityIncreaseUs;
  private final long maxDurationForQualityDecreaseUs;
  private final long minDurationToRetainAfterDiscardUs;
  private final float bufferedFractionToLiveEdgeForQualityIncrease;
  private final long minTimeBetweenBufferReevaluationMs;
  private final Clock clock;

  private float playbackSpeed;
  private int selectedIndex;
  private int reason;
  private long lastBufferEvaluationMs;

  /**
   * @param group The {@link TrackGroup}.
   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
   *     empty. May be in any order.
   * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
   */
  public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
      BandwidthMeter bandwidthMeter) {
    this(
        group,
        tracks,
        bandwidthMeter,
        /* reservedBandwidth= */ 0,
        DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
        DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
        DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
        DEFAULT_BANDWIDTH_FRACTION,
        DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
        DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
        Clock.DEFAULT);
  }

  /**
   * @param group The {@link TrackGroup}.
   * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be
   *     empty. May be in any order.
   * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
   * @param reservedBandwidth The reserved bandwidth, which shouldn't be considered available for
   *     use, in bits per second.
   * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
   *     selected track to switch to one of higher quality.
   * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
   *     selected track to switch to one of lower quality.
   * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
   *     quality, the selection may indicate that media already buffered at the lower quality can be
   *     discarded to speed up the switch. This is the minimum duration of media that must be
   *     retained at the lower quality.
   * @param bandwidthFraction The fraction of the available bandwidth that the selection should
   *     consider available for use. Setting to a value less than 1 is recommended to account for
   *     inaccuracies in the bandwidth estimator.
   * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
   *     duration from current playback position to the live edge that has to be buffered before the
   *     selected track can be switched to one of higher quality. This parameter is only applied
   *     when the playback position is closer to the live edge than {@code
   *     minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
   *     quality from happening.
   * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
   *     buffer and discard some chunks of lower quality to improve the playback quality if network
   *     condition has changed. This is the minimum duration between 2 consecutive buffer
   *     reevaluation calls.
   */
  public AdaptiveTrackSelection(
      TrackGroup group,
      int[] tracks,
      BandwidthMeter bandwidthMeter,
      long reservedBandwidth,
      long minDurationForQualityIncreaseMs,
      long maxDurationForQualityDecreaseMs,
      long minDurationToRetainAfterDiscardMs,
      float bandwidthFraction,
      float bufferedFractionToLiveEdgeForQualityIncrease,
      long minTimeBetweenBufferReevaluationMs,
      Clock clock) {
    this(
        group,
        tracks,
        new DefaultBandwidthProvider(bandwidthMeter, bandwidthFraction, reservedBandwidth),
        minDurationForQualityIncreaseMs,
        maxDurationForQualityDecreaseMs,
        minDurationToRetainAfterDiscardMs,
        bufferedFractionToLiveEdgeForQualityIncrease,
        minTimeBetweenBufferReevaluationMs,
        clock);
  }

  private AdaptiveTrackSelection(
      TrackGroup group,
      int[] tracks,
      BandwidthProvider bandwidthProvider,
      long minDurationForQualityIncreaseMs,
      long maxDurationForQualityDecreaseMs,
      long minDurationToRetainAfterDiscardMs,
      float bufferedFractionToLiveEdgeForQualityIncrease,
      long minTimeBetweenBufferReevaluationMs,
      Clock clock) {
    super(group, tracks);
    this.bandwidthProvider = bandwidthProvider;
    this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
    this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
    this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
    this.bufferedFractionToLiveEdgeForQualityIncrease =
        bufferedFractionToLiveEdgeForQualityIncrease;
    this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
    this.clock = clock;
    playbackSpeed = 1f;
    reason = C.SELECTION_REASON_UNKNOWN;
    lastBufferEvaluationMs = C.TIME_UNSET;
  }

  /**
   * Sets checkpoints to determine the allocation bandwidth based on the total bandwidth.
   *
   * @param allocationCheckpoints List of checkpoints. Each element must be a long[2], with [0]
   *     being the total bandwidth and [1] being the allocated bandwidth.
   */
  public void experimental_setBandwidthAllocationCheckpoints(long[][] allocationCheckpoints) {
    ((DefaultBandwidthProvider) bandwidthProvider)
        .experimental_setBandwidthAllocationCheckpoints(allocationCheckpoints);
  }

  @Override
  public void enable() {
    lastBufferEvaluationMs = C.TIME_UNSET;
  }

  @Override
  public void onPlaybackSpeed(float playbackSpeed) {
    this.playbackSpeed = playbackSpeed;
  }

  @Override
  public void updateSelectedTrack(
      long playbackPositionUs,
      long bufferedDurationUs,
      long availableDurationUs,
      List<? extends MediaChunk> queue,
      MediaChunkIterator[] mediaChunkIterators) {
    long nowMs = clock.elapsedRealtime();

    // Make initial selection
    if (reason == C.SELECTION_REASON_UNKNOWN) {
      reason = C.SELECTION_REASON_INITIAL;
      selectedIndex = determineIdealSelectedIndex(nowMs);
      return;
    }

    // Stash the current selection, then make a new one.
    int currentSelectedIndex = selectedIndex;
    selectedIndex = determineIdealSelectedIndex(nowMs);
    if (selectedIndex == currentSelectedIndex) {
      return;
    }

    if (!isBlacklisted(currentSelectedIndex, nowMs)) {
      // Revert back to the current selection if conditions are not suitable for switching.
      Format currentFormat = getFormat(currentSelectedIndex);
      Format selectedFormat = getFormat(selectedIndex);
      if (selectedFormat.bitrate > currentFormat.bitrate
          && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) {
        // The selected track is a higher quality, but we have insufficient buffer to safely switch
        // up. Defer switching up for now.
        selectedIndex = currentSelectedIndex;
      } else if (selectedFormat.bitrate < currentFormat.bitrate
          && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
        // The selected track is a lower quality, but we have sufficient buffer to defer switching
        // down for now.
        selectedIndex = currentSelectedIndex;
      }
    }
    // If we adapted, update the trigger.
    if (selectedIndex != currentSelectedIndex) {
      reason = C.SELECTION_REASON_ADAPTIVE;
    }
  }

  @Override
  public int getSelectedIndex() {
    return selectedIndex;
  }

  @Override
  public int getSelectionReason() {
    return reason;
  }

  @Override
  @Nullable
  public Object getSelectionData() {
    return null;
  }

  @Override
  public int evaluateQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) {
    long nowMs = clock.elapsedRealtime();
    if (!shouldEvaluateQueueSize(nowMs)) {
      return queue.size();
    }

    lastBufferEvaluationMs = nowMs;
    if (queue.isEmpty()) {
      return 0;
    }

    int queueSize = queue.size();
    MediaChunk lastChunk = queue.get(queueSize - 1);
    long playoutBufferedDurationBeforeLastChunkUs =
        Util.getPlayoutDurationForMediaDuration(
            lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
    long minDurationToRetainAfterDiscardUs = getMinDurationToRetainAfterDiscardUs();
    if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
      return queueSize;
    }
    int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
    Format idealFormat = getFormat(idealSelectedIndex);
    // If the chunks contain video, discard from the first SD chunk beyond
    // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
    // track.
    for (int i = 0; i < queueSize; i++) {
      MediaChunk chunk = queue.get(i);
      Format format = chunk.trackFormat;
      long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
      long playoutDurationBeforeThisChunkUs =
          Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed);
      if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
          && format.bitrate < idealFormat.bitrate
          && format.height != Format.NO_VALUE && format.height < 720
          && format.width != Format.NO_VALUE && format.width < 1280
          && format.height < idealFormat.height) {
        return i;
      }
    }
    return queueSize;
  }

  /**
   * Called when updating the selected track to determine whether a candidate track can be selected.
   *
   * @param format The {@link Format} of the candidate track.
   * @param trackBitrate The estimated bitrate of the track. May differ from {@link Format#bitrate}
   *     if a more accurate estimate of the current track bitrate is available.
   * @param playbackSpeed The current playback speed.
   * @param effectiveBitrate The bitrate available to this selection.
   * @return Whether this {@link Format} can be selected.
   */
  @SuppressWarnings("unused")
  protected boolean canSelectFormat(
      Format format, int trackBitrate, float playbackSpeed, long effectiveBitrate) {
    return Math.round(trackBitrate * playbackSpeed) <= effectiveBitrate;
  }

  /**
   * Called from {@link #evaluateQueueSize(long, List)} to determine whether an evaluation should be
   * performed.
   *
   * @param nowMs The current value of {@link Clock#elapsedRealtime()}.
   * @return Whether an evaluation should be performed.
   */
  protected boolean shouldEvaluateQueueSize(long nowMs) {
    return lastBufferEvaluationMs == C.TIME_UNSET
        || nowMs - lastBufferEvaluationMs >= minTimeBetweenBufferReevaluationMs;
  }

  /**
   * Called from {@link #evaluateQueueSize(long, List)} to determine the minimum duration of buffer
   * to retain after discarding chunks.
   *
   * @return The minimum duration of buffer to retain after discarding chunks, in microseconds.
   */
  protected long getMinDurationToRetainAfterDiscardUs() {
    return minDurationToRetainAfterDiscardUs;
  }

  /**
   * Computes the ideal selected index ignoring buffer health.
   *
   * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
   *     Long#MIN_VALUE} to ignore blacklisting.
   */
  private int determineIdealSelectedIndex(long nowMs) {
    long effectiveBitrate = bandwidthProvider.getAllocatedBandwidth();
    int lowestBitrateNonBlacklistedIndex = 0;
    for (int i = 0; i < length; i++) {
      if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
        Format format = getFormat(i);
        if (canSelectFormat(format, format.bitrate, playbackSpeed, effectiveBitrate)) {
          return i;
        } else {
          lowestBitrateNonBlacklistedIndex = i;
        }
      }
    }
    return lowestBitrateNonBlacklistedIndex;
  }

  private long minDurationForQualityIncreaseUs(long availableDurationUs) {
    boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET
        && availableDurationUs <= minDurationForQualityIncreaseUs;
    return isAvailableDurationTooShort
        ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease)
        : minDurationForQualityIncreaseUs;
  }

  /** Provides the allocated bandwidth. */
  private interface BandwidthProvider {

    /** Returns the allocated bitrate. */
    long getAllocatedBandwidth();
  }

  private static final class DefaultBandwidthProvider implements BandwidthProvider {

    private final BandwidthMeter bandwidthMeter;
    private final float bandwidthFraction;
    private final long reservedBandwidth;

    @Nullable private long[][] allocationCheckpoints;

    /* package */
    // the constructor does not initialize fields: allocationCheckpoints
    @SuppressWarnings("nullness:initialization.fields.uninitialized")
    DefaultBandwidthProvider(
        BandwidthMeter bandwidthMeter, float bandwidthFraction, long reservedBandwidth) {
      this.bandwidthMeter = bandwidthMeter;
      this.bandwidthFraction = bandwidthFraction;
      this.reservedBandwidth = reservedBandwidth;
    }

    // unboxing a possibly-null reference allocationCheckpoints[nextIndex][0]
    @SuppressWarnings("nullness:unboxing.of.nullable")
    @Override
    public long getAllocatedBandwidth() {
      long totalBandwidth = (long) (bandwidthMeter.getBitrateEstimate() * bandwidthFraction);
      long allocatableBandwidth = Math.max(0L, totalBandwidth - reservedBandwidth);
      if (allocationCheckpoints == null) {
        return allocatableBandwidth;
      }
      int nextIndex = 1;
      while (nextIndex < allocationCheckpoints.length - 1
          && allocationCheckpoints[nextIndex][0] < allocatableBandwidth) {
        nextIndex++;
      }
      long[] previous = allocationCheckpoints[nextIndex - 1];
      long[] next = allocationCheckpoints[nextIndex];
      float fractionBetweenCheckpoints =
          (float) (allocatableBandwidth - previous[0]) / (next[0] - previous[0]);
      return previous[1] + (long) (fractionBetweenCheckpoints * (next[1] - previous[1]));
    }

    /* package */ void experimental_setBandwidthAllocationCheckpoints(
        long[][] allocationCheckpoints) {
      Assertions.checkArgument(allocationCheckpoints.length >= 2);
      this.allocationCheckpoints = allocationCheckpoints;
    }
  }

  /**
   * Returns allocation checkpoints for allocating bandwidth between multiple adaptive track
   * selections.
   *
   * @param trackBitrates Array of [selectionIndex][trackIndex] -> trackBitrate.
   * @return Array of allocation checkpoints [selectionIndex][checkpointIndex][2] with [0]=total
   *     bandwidth at checkpoint and [1]=allocated bandwidth at checkpoint.
   */
  private static long[][][] getAllocationCheckpoints(long[][] trackBitrates) {
    // Algorithm:
    //  1. Use log bitrates to treat all resolution update steps equally.
    //  2. Distribute switch points for each selection equally in the same [0.0-1.0] range.
    //  3. Switch up one format at a time in the order of the switch points.
    double[][] logBitrates = getLogArrayValues(trackBitrates);
    double[][] switchPoints = getSwitchPoints(logBitrates);

    // There will be (count(switch point) + 3) checkpoints:
    // [0] = all zero, [1] = minimum bitrates, [2-(end-1)] = up-switch points,
    // [end] = extra point to set slope for additional bitrate.
    int checkpointCount = countArrayElements(switchPoints) + 3;
    long[][][] checkpoints = new long[logBitrates.length][checkpointCount][2];
    int[] currentSelection = new int[logBitrates.length];
    setCheckpointValues(checkpoints, /* checkpointIndex= */ 1, trackBitrates, currentSelection);
    for (int checkpointIndex = 2; checkpointIndex < checkpointCount - 1; checkpointIndex++) {
      int nextUpdateIndex = 0;
      double nextUpdateSwitchPoint = Double.MAX_VALUE;
      for (int i = 0; i < logBitrates.length; i++) {
        if (currentSelection[i] + 1 == logBitrates[i].length) {
          continue;
        }
        double switchPoint = switchPoints[i][currentSelection[i]];
        if (switchPoint < nextUpdateSwitchPoint) {
          nextUpdateSwitchPoint = switchPoint;
          nextUpdateIndex = i;
        }
      }
      currentSelection[nextUpdateIndex]++;
      setCheckpointValues(checkpoints, checkpointIndex, trackBitrates, currentSelection);
    }
    for (long[][] points : checkpoints) {
      points[checkpointCount - 1][0] = 2 * points[checkpointCount - 2][0];
      points[checkpointCount - 1][1] = 2 * points[checkpointCount - 2][1];
    }
    return checkpoints;
  }

  /** Converts all input values to Math.log(value). */
  private static double[][] getLogArrayValues(long[][] values) {
    double[][] logValues = new double[values.length][];
    for (int i = 0; i < values.length; i++) {
      logValues[i] = new double[values[i].length];
      for (int j = 0; j < values[i].length; j++) {
        logValues[i][j] = values[i][j] == Format.NO_VALUE ? 0 : Math.log(values[i][j]);
      }
    }
    return logValues;
  }

  /**
   * Returns idealized switch points for each switch between consecutive track selection bitrates.
   *
   * @param logBitrates Log bitrates with [selectionCount][formatCount].
   * @return Linearly distributed switch points in the range of [0.0-1.0].
   */
  private static double[][] getSwitchPoints(double[][] logBitrates) {
    double[][] switchPoints = new double[logBitrates.length][];
    for (int i = 0; i < logBitrates.length; i++) {
      switchPoints[i] = new double[logBitrates[i].length - 1];
      if (switchPoints[i].length == 0) {
        continue;
      }
      double totalBitrateDiff = logBitrates[i][logBitrates[i].length - 1] - logBitrates[i][0];
      for (int j = 0; j < logBitrates[i].length - 1; j++) {
        double switchBitrate = 0.5 * (logBitrates[i][j] + logBitrates[i][j + 1]);
        switchPoints[i][j] =
            totalBitrateDiff == 0.0 ? 1.0 : (switchBitrate - logBitrates[i][0]) / totalBitrateDiff;
      }
    }
    return switchPoints;
  }

  /** Returns total number of elements in a 2D array. */
  private static int countArrayElements(double[][] array) {
    int count = 0;
    for (double[] subArray : array) {
      count += subArray.length;
    }
    return count;
  }

  /**
   * Sets checkpoint bitrates.
   *
   * @param checkpoints Output checkpoints with [selectionIndex][checkpointIndex][2] where [0]=Total
   *     bitrate and [1]=Allocated bitrate.
   * @param checkpointIndex The checkpoint index.
   * @param trackBitrates The track bitrates with [selectionIndex][trackIndex].
   * @param selectedTracks The indices of selected tracks for each selection for this checkpoint.
   */
  private static void setCheckpointValues(
      long[][][] checkpoints, int checkpointIndex, long[][] trackBitrates, int[] selectedTracks) {
    long totalBitrate = 0;
    for (int i = 0; i < checkpoints.length; i++) {
      checkpoints[i][checkpointIndex][1] = trackBitrates[i][selectedTracks[i]];
      totalBitrate += checkpoints[i][checkpointIndex][1];
    }
    for (long[][] points : checkpoints) {
      points[checkpointIndex][0] = totalBitrate;
    }
  }
}