diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 2ea9a9871c..312f2ac3b3 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -364,15 +364,8 @@ public class EventLogger implements ExoPlayer.EventListener, SimpleExoPlayer.Deb private static String getTrackStatusString(TrackSelection selection, TrackGroup group, int trackIndex) { - boolean groupEnabled = selection != null && selection.group == group; - if (groupEnabled) { - for (int i = 0; i < selection.length; i++) { - if (selection.getTrack(i) == trackIndex) { - return getTrackStatusString(true); - } - } - } - return getTrackStatusString(false); + return getTrackStatusString(selection != null && selection.getTrackGroup() == group + && selection.indexOf(trackIndex) != -1); } private static String getTrackStatusString(boolean enabled) { diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ac35acf00e..d01fb004f3 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -39,8 +39,6 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -48,13 +46,15 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.ui.SubtitleView; +import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo; +import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.PlayerControl; +import com.google.android.exoplayer2.ui.SubtitleView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -138,7 +138,7 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, private String userAgent; private DataSource.Factory manifestDataSourceFactory; private DataSource.Factory mediaDataSourceFactory; - private FormatEvaluator.Factory formatEvaluatorFactory; + private DefaultBandwidthMeter bandwidthMeter; private SimpleExoPlayer player; private MappingTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; @@ -155,9 +155,8 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, super.onCreate(savedInstanceState); userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); manifestDataSourceFactory = new DefaultDataSourceFactory(this, userAgent); - DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + bandwidthMeter = new DefaultBandwidthMeter(); mediaDataSourceFactory = new DefaultDataSourceFactory(this, userAgent, bandwidthMeter); - formatEvaluatorFactory = new AdaptiveEvaluator.Factory(bandwidthMeter); mainHandler = new Handler(); setContentView(R.layout.player_activity); @@ -284,12 +283,15 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, return; } } + eventLogger = new EventLogger(); eventLogger.startSession(); - trackSelector = new DefaultTrackSelector(mainHandler); + TrackSelection.Factory videoTrackSelectionFactory = + new AdaptiveVideoTrackSelection.Factory(bandwidthMeter); + trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory); trackSelector.addListener(this); trackSelector.addListener(eventLogger); - trackSelectionHelper = new TrackSelectionHelper(trackSelector); + trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), drmSessionManager, preferExtensionDecoders); player.addListener(this); @@ -354,15 +356,14 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, switch (type) { case Util.TYPE_SS: DefaultSsChunkSource.Factory factory = new DefaultSsChunkSource.Factory( - mediaDataSourceFactory, formatEvaluatorFactory); + mediaDataSourceFactory); return new SsMediaSource(uri, manifestDataSourceFactory, factory, mainHandler, eventLogger); case Util.TYPE_DASH: DefaultDashChunkSource.Factory factory2 = new DefaultDashChunkSource.Factory( - mediaDataSourceFactory, formatEvaluatorFactory); + mediaDataSourceFactory); return new DashMediaSource(uri, mediaDataSourceFactory, factory2, mainHandler, eventLogger); case Util.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, formatEvaluatorFactory, mainHandler, - eventLogger); + return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); case Util.TYPE_OTHER: return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index ca3ed9f8b5..e8d856a68e 100644 --- a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -19,8 +19,10 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.TrackInfo; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.MimeTypes; @@ -45,6 +47,7 @@ import java.util.Locale; DialogInterface.OnClickListener { private final MappingTrackSelector selector; + private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; private TrackInfo trackInfo; private int rendererIndex; @@ -55,13 +58,18 @@ import java.util.Locale; private CheckedTextView disableView; private CheckedTextView defaultView; + private CheckedTextView enableRandomAdaptationView; private CheckedTextView[][] trackViews; /** * @param selector The track selector. + * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, + * or null if the selection helper should not support adaptive video. */ - public TrackSelectionHelper(MappingTrackSelector selector) { + public TrackSelectionHelper(MappingTrackSelector selector, + TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { this.selector = selector; + this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; } /** @@ -80,8 +88,9 @@ import java.util.Locale; trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroupsAdaptive = new boolean[trackGroups.length]; for (int i = 0; i < trackGroups.length; i++) { - trackGroupsAdaptive[i] = trackInfo.getAdaptiveSupport(rendererIndex, i, false) - != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED; + trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null + && (trackInfo.getAdaptiveSupport(rendererIndex, i, false) + != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED); } isDisabled = selector.getRendererDisabled(rendererIndex); override = selector.hasSelectionOverride(rendererIndex, trackGroups) @@ -118,17 +127,19 @@ import java.util.Locale; // Per-track views. boolean haveSupportedTracks = false; + boolean haveAdaptiveTracks = false; trackViews = new CheckedTextView[trackGroups.length][]; for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup group = trackGroups.get(groupIndex); + boolean groupIsAdaptive = group.length > 1 && trackGroupsAdaptive[groupIndex]; + haveAdaptiveTracks |= groupIsAdaptive; trackViews[groupIndex] = new CheckedTextView[group.length]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { if (trackIndex == 0) { root.addView(inflater.inflate(R.layout.list_divider, root, false)); } - int trackViewLayoutId = group.length < 2 || !trackGroupsAdaptive[groupIndex] - ? android.R.layout.simple_list_item_single_choice - : android.R.layout.simple_list_item_multiple_choice; + int trackViewLayoutId = groupIsAdaptive ? android.R.layout.simple_list_item_multiple_choice + : android.R.layout.simple_list_item_single_choice; CheckedTextView trackView = (CheckedTextView) inflater.inflate( trackViewLayoutId, root, false); trackView.setText(buildTrackName(group.getFormat(trackIndex))); @@ -148,6 +159,14 @@ import java.util.Locale; if (!haveSupportedTracks) { // Indicate that the default selection will be nothing. defaultView.setText(R.string.selection_default_none); + } else if (haveAdaptiveTracks) { + // View for using random adaptation. + enableRandomAdaptationView = (CheckedTextView) inflater.inflate( + android.R.layout.simple_list_item_multiple_choice, root, false); + enableRandomAdaptationView.setText(R.string.enable_random_adaptation); + enableRandomAdaptationView.setOnClickListener(this); + root.addView(inflater.inflate(R.layout.list_divider, root, false)); + root.addView(enableRandomAdaptationView); } updateViews(); @@ -158,9 +177,18 @@ import java.util.Locale; disableView.setChecked(isDisabled); defaultView.setChecked(!isDisabled && override == null); for (int i = 0; i < trackViews.length; i++) { + TrackGroup trackGroup = trackGroups.get(i); for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked( - override != null && override.group == trackGroups.get(i) && override.indexOf(j) != -1); + trackViews[i][j].setChecked(override != null && override.getTrackGroup() == trackGroup + && override.indexOf(trackGroup.getFormat(j)) != -1); + } + } + if (enableRandomAdaptationView != null) { + enableRandomAdaptationView.setEnabled(!isDisabled && override != null + && override.length() > 1); + if (enableRandomAdaptationView.isEnabled()) { + enableRandomAdaptationView.setChecked(!isDisabled + && override instanceof RandomTrackSelection); } } } @@ -191,6 +219,9 @@ import java.util.Locale; } else if (view == defaultView) { isDisabled = false; override = null; + } else if (view == enableRandomAdaptationView) { + setOverride(override.getTrackGroup(), getTracks(override), + !enableRandomAdaptationView.isChecked()); } else { isDisabled = false; @SuppressWarnings("unchecked") @@ -198,31 +229,25 @@ import java.util.Locale; TrackGroup group = tag.first; int trackIndex = tag.second; if (!trackGroupsAdaptive[trackGroups.indexOf(group)] || override == null) { - override = new TrackSelection(group, trackIndex); + override = new FixedTrackSelection(group, trackIndex); } else { // The group being modified is adaptive and we already have a non-null override. boolean isEnabled = ((CheckedTextView) view).isChecked(); + int overrideLength = override.length(); if (isEnabled) { // Remove the track from the override. - if (override.length == 1) { + if (overrideLength == 1) { // The last track is being removed, so the override becomes empty. override = null; isDisabled = true; } else { - int[] tracks = new int[override.length - 1]; - int trackCount = 0; - for (int i = 0; i < override.length; i++) { - if (override.getTrack(i) != trackIndex) { - tracks[trackCount++] = override.getTrack(i); - } - } - override = new TrackSelection(group, tracks); + setOverride(group, getTracksRemoving(override, trackIndex), + enableRandomAdaptationView.isChecked()); } } else { // Add the track to the override. - int[] tracks = Arrays.copyOf(override.getTracks(), override.length + 1); - tracks[tracks.length - 1] = trackIndex; - override = new TrackSelection(group, tracks); + setOverride(group, getTracksAdding(override, trackIndex), + enableRandomAdaptationView.isChecked()); } } } @@ -230,6 +255,41 @@ import java.util.Locale; updateViews(); } + private void setOverride(TrackGroup group, int[] tracks, boolean enableRandomAdaptation) { + override = tracks.length == 1 ? new FixedTrackSelection(group, tracks[0]) + : (enableRandomAdaptation ? new RandomTrackSelection(group, tracks) + : adaptiveVideoTrackSelectionFactory.createTrackSelection(group, tracks)); + } + + // Track array manipulation. + + private static int[] getTracks(TrackSelection trackSelection) { + int[] tracks = new int[trackSelection.length()]; + for (int i = 0; i < tracks.length; i++) { + tracks[i] = trackSelection.getIndexInTrackGroup(i); + } + return tracks; + } + + private static int[] getTracksAdding(TrackSelection trackSelection, int addedTrack) { + int[] tracks = getTracks(trackSelection); + tracks = Arrays.copyOf(tracks, tracks.length + 1); + tracks[tracks.length - 1] = addedTrack; + return tracks; + } + + private static int[] getTracksRemoving(TrackSelection trackSelection, int removedTrack) { + int[] tracks = new int[trackSelection.length() - 1]; + int trackCount = 0; + for (int i = 0; i < tracks.length + 1; i++) { + int track = trackSelection.getIndexInTrackGroup(i); + if (track != removedTrack) { + tracks[trackCount++] = track; + } + } + return tracks; + } + // Track name construction. private static String buildTrackName(Format format) { diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index 6d38b984a9..01b5221a05 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -33,6 +33,8 @@ Default (none) + Enable random adaptation + Protected content not supported on API levels below 18 This device does not support the required DRM scheme diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 0944584de6..84a2441b8f 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -834,9 +834,9 @@ import java.util.ArrayList; if (newSelection != null) { // Replace the renderer's SampleStream so the transition to playing the next period // can be seamless. - Format[] formats = new Format[newSelection.length]; + Format[] formats = new Format[newSelection.length()]; for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.group.getFormat(newSelection.getTrack(j)); + formats[j] = newSelection.getFormat(j); } renderer.replaceStream(formats, readingPeriod.sampleStreams[i], readingPeriod.offsetUs); @@ -1087,9 +1087,9 @@ import java.util.ArrayList; // Consider as joining only if the renderer was previously disabled. boolean joining = !rendererWasEnabledFlags[i] && playing; // Build an array of formats contained by the selection. - Format[] formats = new Format[newSelection.length]; + Format[] formats = new Format[newSelection.length()]; for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.group.getFormat(newSelection.getTrack(j)); + formats[j] = newSelection.getFormat(j); } // Enable the renderer. renderer.enable(formats, playingPeriod.sampleStreams[i], internalPositionUs, joining, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 8ac1495f66..81167337b8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -250,9 +250,9 @@ public final class ExtractorMediaSource implements MediaPeriod, MediaSource, SampleStream[] newStreams = new SampleStream[newSelections.size()]; for (int i = 0; i < newStreams.length; i++) { TrackSelection selection = newSelections.get(i); - Assertions.checkState(selection.length == 1); - Assertions.checkState(selection.getTrack(0) == 0); - int track = tracks.indexOf(selection.group); + Assertions.checkState(selection.length() == 1); + Assertions.checkState(selection.getIndexInTrackGroup(0) == 0); + int track = tracks.indexOf(selection.getTrackGroup()); Assertions.checkState(!trackEnabledStates[track]); enabledTrackCount++; trackEnabledStates[track] = true; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 5e9b81523c..77079cbc16 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -220,7 +220,7 @@ public final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callba TrackGroupArray periodTrackGroups = period.getTrackGroups(); for (int i = 0; i < allNewSelections.size(); i++) { TrackSelection selection = allNewSelections.get(i); - if (periodTrackGroups.indexOf(selection.group) != -1) { + if (periodTrackGroups.indexOf(selection.getTrackGroup()) != -1) { newSelectionOriginalIndices[newSelections.size()] = i; newSelections.add(selection); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 00575d4afc..b8e6f9fa35 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -150,7 +150,6 @@ public class ChunkSampleStream implements SampleStream, S * This method should be called when the stream is no longer required. */ public void release() { - chunkSource.release(); sampleQueue.disable(); loader.release(); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java index df3654f80d..00865822e1 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java @@ -84,11 +84,4 @@ public interface ChunkSource { */ boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e); - /** - * Releases the source. - *

- * This method should be called when the source is no longer required. - */ - void release(); - } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/chunk/FormatEvaluator.java b/library/src/main/java/com/google/android/exoplayer2/source/chunk/FormatEvaluator.java deleted file mode 100644 index 55a0c2cad6..0000000000 --- a/library/src/main/java/com/google/android/exoplayer2/source/chunk/FormatEvaluator.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * 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 com.google.android.exoplayer2.source.chunk; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.upstream.BandwidthMeter; - -import java.util.List; -import java.util.Random; - -/** - * Selects from a number of available formats during playback. - */ -public interface FormatEvaluator { - - /** - * A factory for {@link FormatEvaluator} instances. - */ - interface Factory { - - /** - * Creates a {@link FormatEvaluator} instance. - */ - FormatEvaluator createFormatEvaluator(); - - } - - /** - * Enables the evaluator. - * - * @param formats The formats from which to select, ordered by decreasing bandwidth. - */ - void enable(Format[] formats); - - /** - * Disables the evaluator. - */ - void disable(); - - /** - * Update the supplied evaluation. - *

- * When called, {@code evaluation} must contain the currently selected format (null for an initial - * evaluation), the most recent reason ({@link C#SELECTION_REASON_INITIAL} for an initial - * evaluation) and the most recent evaluation data (null for an initial evaluation). - * - * @param bufferedDurationUs The duration of media currently buffered in microseconds. - * @param blacklistFlags An array whose length is equal to the number of available formats. A - * {@code true} element indicates that a format is currently blacklisted and should not be - * selected by the evaluation. At least one element must be {@code false}. - * @param evaluation The evaluation to be updated. - */ - void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, - Evaluation evaluation); - - /** - * Evaluates whether to discard {@link MediaChunk}s from the queue. - * - * @param playbackPositionUs The current playback position in microseconds. - * @param queue The queue of buffered {@link MediaChunk}s. - * @param blacklistFlags An array whose length is equal to the number of available formats. A - * {@code true} element indicates that a format is currently blacklisted and should not be - * selected by the evaluation. At least one element must be {@code false}. - * @return The preferred queue size. - */ - int evaluateQueueSize(long playbackPositionUs, List queue, - boolean[] blacklistFlags); - - /** - * A format evaluation. - */ - final class Evaluation { - - /** - * The selected format. - */ - public Format format; - - /** - * The sticky reason for the format selection. - */ - public int reason; - - /** - * Sticky optional data relating to the evaluation. - */ - public Object data; - - public Evaluation() { - reason = C.SELECTION_REASON_INITIAL; - } - - } - - /** - * Selects randomly between the available formats, excluding those that are blacklisted. - */ - final class RandomEvaluator implements FormatEvaluator { - - public static class Factory implements FormatEvaluator.Factory { - - private final int seed; - private final boolean seedIsSet; - - public Factory() { - seed = 0; - seedIsSet = false; - } - - public Factory(int seed) { - this.seed = seed; - seedIsSet = true; - } - - @Override - public FormatEvaluator createFormatEvaluator() { - return seedIsSet ? new RandomEvaluator(seed) : new RandomEvaluator(); - } - - } - - private final Random random; - - private Format[] formats; - - public RandomEvaluator() { - this.random = new Random(); - } - - /** - * @param seed A seed for the underlying random number generator. - */ - public RandomEvaluator(int seed) { - this.random = new Random(seed); - } - - @Override - public void enable(Format[] formats) { - this.formats = formats; - } - - @Override - public void disable() { - formats = null; - } - - @Override - public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, - Evaluation evaluation) { - // Count the number of non-blacklisted formats. - int nonBlacklistedFormatCount = 0; - for (boolean blacklistFlag : blacklistFlags) { - if (!blacklistFlag) { - nonBlacklistedFormatCount++; - } - } - - int formatIndex = random.nextInt(nonBlacklistedFormatCount); - if (nonBlacklistedFormatCount != formats.length) { - // Adjust the format index to account for blacklisted formats. - nonBlacklistedFormatCount = 0; - for (int i = 0; i < blacklistFlags.length; i++) { - if (!blacklistFlags[i] && formatIndex == nonBlacklistedFormatCount++) { - formatIndex = i; - break; - } - } - } - Format newFormat = formats[formatIndex]; - if (evaluation.format != null && evaluation.format != newFormat) { - evaluation.reason = C.SELECTION_REASON_ADAPTIVE; - } - evaluation.format = newFormat; - } - - @Override - public int evaluateQueueSize(long playbackPositionUs, List queue, - boolean[] blacklistFlags) { - return queue.size(); - } - - } - - /** - * An adaptive evaluator for video formats, which attempts to select the best quality possible - * given the current network conditions and state of the buffer. - *

- * This implementation should be used for video only, and should not be used for audio. It is a - * reference implementation only. It is recommended that application developers implement their - * own adaptive evaluator to more precisely suit their use case. - */ - final class AdaptiveEvaluator implements FormatEvaluator { - - public static class Factory implements FormatEvaluator.Factory { - - private final BandwidthMeter bandwidthMeter; - - public Factory(BandwidthMeter bandwidthMeter) { - this.bandwidthMeter = bandwidthMeter; - } - - @Override - public FormatEvaluator createFormatEvaluator() { - return new AdaptiveEvaluator(bandwidthMeter); - } - - } - - private static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; - - private static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000; - private static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; - private static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; - private static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; - - private final BandwidthMeter bandwidthMeter; - private final int maxInitialBitrate; - private final long minDurationForQualityIncreaseUs; - private final long maxDurationForQualityDecreaseUs; - private final long minDurationToRetainAfterDiscardUs; - private final float bandwidthFraction; - - private Format[] formats; - - /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - */ - public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) { - this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, - 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); - } - - /** - * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed - * when bandwidthMeter cannot provide an estimate due to playback having only just started. - * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for - * the evaluator to consider switching to a higher quality format. - * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for - * the evaluator to consider switching to a lower quality format. - * @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality - * format, the evaluator may discard some of the media that it has already buffered at the - * lower quality, so as to switch up to the higher quality faster. 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 evaluator should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. - */ - public AdaptiveEvaluator(BandwidthMeter bandwidthMeter, - int maxInitialBitrate, - int minDurationForQualityIncreaseMs, - int maxDurationForQualityDecreaseMs, - int minDurationToRetainAfterDiscardMs, - float bandwidthFraction) { - this.bandwidthMeter = bandwidthMeter; - this.maxInitialBitrate = maxInitialBitrate; - this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; - this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; - this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; - this.bandwidthFraction = bandwidthFraction; - } - - @Override - public void enable(Format[] formats) { - this.formats = formats; - } - - @Override - public void disable() { - formats = null; - } - - @Override - public void evaluateFormat(long bufferedDurationUs, boolean[] blacklistFlags, - Evaluation evaluation) { - Format current = evaluation.format; - Format selected = determineIdealFormat(formats, blacklistFlags, - bandwidthMeter.getBitrateEstimate()); - if (current != null && isEnabledFormat(current, blacklistFlags)) { - if (selected.bitrate > current.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs) { - // The ideal format is a higher quality, but we have insufficient buffer to safely switch - // up. Defer switching up for now. - selected = current; - } else if (selected.bitrate < current.bitrate - && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { - // The ideal format is a lower quality, but we have sufficient buffer to defer switching - // down for now. - selected = current; - } - } - if (current != null && selected != current) { - evaluation.reason = C.SELECTION_REASON_ADAPTIVE; - } - evaluation.format = selected; - } - - @Override - public int evaluateQueueSize(long playbackPositionUs, List queue, - boolean[] blacklistFlags) { - if (queue.isEmpty()) { - return 0; - } - int queueSize = queue.size(); - long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { - return queueSize; - } - Format current = queue.get(queueSize - 1).trackFormat; - Format ideal = determineIdealFormat(formats, blacklistFlags, - bandwidthMeter.getBitrateEstimate()); - if (ideal.bitrate <= current.bitrate) { - return queueSize; - } - // Discard from the first SD chunk beyond minDurationToRetainAfterDiscardUs whose resolution - // and bitrate are both lower than the ideal format. - for (int i = 0; i < queueSize; i++) { - MediaChunk thisChunk = queue.get(i); - long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs; - if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs - && thisChunk.trackFormat.bitrate < ideal.bitrate - && thisChunk.trackFormat.height < ideal.height - && thisChunk.trackFormat.height < 720 - && thisChunk.trackFormat.width < 1280) { - // Discard chunks from this one onwards. - return i; - } - } - return queueSize; - } - - /** - * Compute the ideal format ignoring buffer health. - */ - private Format determineIdealFormat(Format[] formats, boolean[] blacklistFlags, - long bitrateEstimate) { - int lowestBitrateNonBlacklistedIndex = 0; - long effectiveBitrate = bitrateEstimate == BandwidthMeter.NO_ESTIMATE - ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction); - for (int i = 0; i < formats.length; i++) { - Format format = formats[i]; - if (!blacklistFlags[i]) { - if (format.bitrate <= effectiveBitrate) { - return format; - } else { - lowestBitrateNonBlacklistedIndex = i; - } - } - } - return formats[lowestBitrateNonBlacklistedIndex]; - } - - private boolean isEnabledFormat(Format format, boolean[] blacklistFlags) { - for (int i = 0; i < formats.length; i++) { - if (format == formats[i]) { - return !blacklistFlags[i]; - } - } - return false; - } - - } - -} diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index b65272e997..ef3e7cdb16 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -222,7 +222,7 @@ import java.util.List; private ChunkSampleStream buildSampleStream(TrackSelection selection, long positionUs) { - int adaptationSetIndex = trackGroups.indexOf(selection.group); + int adaptationSetIndex = trackGroups.indexOf(selection.getTrackGroup()); AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex); DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, index, adaptationSetIndex, selection, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 04f1a8d227..0a2d6e0b0e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -28,8 +28,6 @@ import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; @@ -56,25 +54,19 @@ public class DefaultDashChunkSource implements DashChunkSource { public static final class Factory implements DashChunkSource.Factory { - private final FormatEvaluator.Factory formatEvaluatorFactory; private final DataSource.Factory dataSourceFactory; - public Factory(DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory) { + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; } @Override public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, long elapsedRealtimeOffsetMs) { - FormatEvaluator adaptiveEvaluator = trackSelection.length > 1 - ? formatEvaluatorFactory.createFormatEvaluator() : null; DataSource dataSource = dataSourceFactory.createDataSource(); - return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndex, trackSelection, dataSource, adaptiveEvaluator, - elapsedRealtimeOffsetMs); + return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, + adaptationSetIndex, trackSelection, dataSource, elapsedRealtimeOffsetMs); } } @@ -83,16 +75,12 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int adaptationSetIndex; private final TrackSelection trackSelection; private final RepresentationHolder[] representationHolders; - private final boolean[] adaptiveFormatBlacklistFlags; private final DataSource dataSource; - private final FormatEvaluator adaptiveFormatEvaluator; private final long elapsedRealtimeOffsetUs; - private final Evaluation evaluation; private DashManifest manifest; private int periodIndex; - private boolean lastChunkWasInitialization; private IOException fatalError; private boolean missingLastSegment; @@ -103,38 +91,28 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param adaptationSetIndex The index of the adaptation set in the period. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified * as the server's unix time minus the local elapsed time. If unknown, set to 0. */ public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, DashManifest manifest, int periodIndex, int adaptationSetIndex, TrackSelection trackSelection, - DataSource dataSource, FormatEvaluator adaptiveFormatEvaluator, - long elapsedRealtimeOffsetMs) { + DataSource dataSource, long elapsedRealtimeOffsetMs) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndex = adaptationSetIndex; this.trackSelection = trackSelection; this.dataSource = dataSource; - this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; this.periodIndex = periodIndex; this.elapsedRealtimeOffsetUs = elapsedRealtimeOffsetMs * 1000; - this.evaluation = new Evaluation(); long periodDurationUs = getPeriodDurationUs(); List representations = getRepresentations(); - representationHolders = new RepresentationHolder[trackSelection.length]; - for (int i = 0; i < trackSelection.length; i++) { - Representation representation = representations.get(trackSelection.getTrack(i)); + representationHolders = new RepresentationHolder[trackSelection.length()]; + for (int i = 0; i < representationHolders.length; i++) { + Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i] = new RepresentationHolder(periodDurationUs, representation); } - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.enable(trackSelection.getFormats()); - adaptiveFormatBlacklistFlags = new boolean[trackSelection.length]; - } else { - adaptiveFormatBlacklistFlags = null; - } } @Override @@ -144,8 +122,8 @@ public class DefaultDashChunkSource implements DashChunkSource { periodIndex = newPeriodIndex; long periodDurationUs = getPeriodDurationUs(); List representations = getRepresentations(); - for (int i = 0; i < trackSelection.length; i++) { - Representation representation = representations.get(trackSelection.getTrack(i)); + for (int i = 0; i < representationHolders.length; i++) { + Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); representationHolders[i].updateRepresentation(periodDurationUs, representation); } } catch (BehindLiveWindowException e) { @@ -164,11 +142,10 @@ public class DefaultDashChunkSource implements DashChunkSource { @Override public int getPreferredQueueSize(long playbackPositionUs, List queue) { - if (fatalError != null || trackSelection.length < 2) { + if (fatalError != null || trackSelection.length() < 2) { return queue.size(); } - return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, - adaptiveFormatBlacklistFlags); + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); } @Override @@ -177,25 +154,11 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - if (evaluation.format == null || !lastChunkWasInitialization) { - if (trackSelection.length > 1) { - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, - evaluation); - } else { - evaluation.format = trackSelection.getFormat(0); - evaluation.reason = C.SELECTION_REASON_UNKNOWN; - evaluation.data = null; - } - } - - Format selectedFormat = evaluation.format; - if (selectedFormat == null) { - return; - } + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + trackSelection.updateSelectedTrack(bufferedDurationUs); RepresentationHolder representationHolder = - representationHolders[trackSelection.indexOf(selectedFormat)]; + representationHolders[trackSelection.getSelectedIndex()]; Representation selectedRepresentation = representationHolder.representation; DashSegmentIndex segmentIndex = representationHolder.segmentIndex; @@ -211,9 +174,8 @@ public class DefaultDashChunkSource implements DashChunkSource { if (pendingInitializationUri != null || pendingIndexUri != null) { // We have initialization and/or index requests to make. Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource, - selectedFormat, evaluation.reason, evaluation.data, pendingInitializationUri, - pendingIndexUri); - lastChunkWasInitialization = true; + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); out.chunk = initializationChunk; return; } @@ -256,9 +218,9 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, selectedFormat, - evaluation.reason, evaluation.data, sampleFormat, segmentNum); - lastChunkWasInitialization = false; + Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), sampleFormat, segmentNum); out.chunk = nextMediaChunk; } @@ -303,13 +265,6 @@ public class DefaultDashChunkSource implements DashChunkSource { return false; } - @Override - public void release() { - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.disable(); - } - } - // Private methods. private List getRepresentations() { diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index f254f82854..1b37d20ac8 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -27,11 +27,10 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.DataChunk; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.Variant; +import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -73,8 +72,6 @@ public class HlsChunkSource { private final String baseUri; private final DataSource dataSource; - private final FormatEvaluator formatEvaluator; - private final Evaluation evaluation; private final HlsPlaylistParser playlistParser; private final PtsTimestampAdjusterProvider timestampAdjusterProvider; private final Variant[] variants; @@ -82,8 +79,6 @@ public class HlsChunkSource { private final TrackGroup trackGroup; private final long[] variantLastPlaylistLoadTimesMs; - private boolean seenFirstExternalTrackSelection; - private boolean formatEvaluatorEnabled; private byte[] scratchSpace; private boolean live; private long durationUs; @@ -94,10 +89,10 @@ public class HlsChunkSource { private String encryptionIvString; private byte[] encryptionIv; - // Properties of enabled variants. + // Note: The track group in the selection is typically *not* equal to trackGroup. This is due to + // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods + // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; - private long[] enabledVariantBlacklistTimes; - private boolean[] enabledVariantBlacklistFlags; /** * @param baseUri The playlist's base uri. @@ -106,17 +101,14 @@ public class HlsChunkSource { * @param timestampAdjusterProvider A provider of {@link PtsTimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. - * @param formatEvaluator For adaptive tracks, selects from the available formats. */ public HlsChunkSource(String baseUri, Variant[] variants, DataSource dataSource, - PtsTimestampAdjusterProvider timestampAdjusterProvider, FormatEvaluator formatEvaluator) { + PtsTimestampAdjusterProvider timestampAdjusterProvider) { this.baseUri = baseUri; this.variants = variants; this.dataSource = dataSource; - this.formatEvaluator = formatEvaluator; this.timestampAdjusterProvider = timestampAdjusterProvider; playlistParser = new HlsPlaylistParser(); - evaluation = new Evaluation(); variantPlaylists = new HlsMediaPlaylist[variants.length]; variantLastPlaylistLoadTimesMs = new long[variants.length]; @@ -127,7 +119,7 @@ public class HlsChunkSource { initialTrackSelection[i] = i; } trackGroup = new TrackGroup(variantFormats); - selectTracksInternal(new TrackSelection(trackGroup, initialTrackSelection), false); + trackSelection = new InitializationTrackSelection(trackGroup, initialTrackSelection); } /** @@ -169,7 +161,7 @@ public class HlsChunkSource { * @param trackSelection The track selection. */ public void selectTracks(TrackSelection trackSelection) { - selectTracksInternal(trackSelection, true); + this.trackSelection = trackSelection; } /** @@ -195,13 +187,14 @@ public class HlsChunkSource { public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, ChunkHolder out) { int previousChunkVariantIndex = previous != null ? trackGroup.indexOf(previous.trackFormat) : -1; - updateFormatEvaluation(previous, playbackPositionUs); - int newVariantIndex = trackGroup.indexOf(evaluation.format); + updateSelectedTrack(previous, playbackPositionUs); + int newVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = previousChunkVariantIndex != newVariantIndex; HlsMediaPlaylist mediaPlaylist = variantPlaylists[newVariantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. - out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.reason, evaluation.data); + out.chunk = newMediaPlaylistChunk(newVariantIndex, trackSelection.getSelectionReason(), + trackSelection.getSelectionData()); return; } @@ -236,7 +229,8 @@ public class HlsChunkSource { if (!mediaPlaylist.live) { out.endOfStream = true; } else if (shouldRerequestLiveMediaPlaylist(newVariantIndex)) { - out.chunk = newMediaPlaylistChunk(newVariantIndex, evaluation.reason, evaluation.data); + out.chunk = newMediaPlaylistChunk(newVariantIndex, + trackSelection.getSelectionReason(), trackSelection.getSelectionData()); } return; } @@ -250,7 +244,7 @@ public class HlsChunkSource { if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, newVariantIndex, - evaluation.reason, evaluation.data); + trackSelection.getSelectionReason(), trackSelection.getSelectionData()); return; } if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { @@ -332,7 +326,8 @@ public class HlsChunkSource { extractorNeedsInit = false; } - out.chunk = new HlsMediaChunk(dataSource, dataSpec, format, evaluation.reason, evaluation.data, + out.chunk = new HlsMediaChunk(dataSource, dataSpec, format, + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber, extractor, extractorNeedsInit, switchingVariant, encryptionKey, encryptionIv); } @@ -412,86 +407,29 @@ public class HlsChunkSource { InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e; int responseCode = responseCodeException.responseCode; if (responseCode == 404 || responseCode == 410) { - int enabledVariantIndex = trackSelection.indexOf(chunk.trackFormat); - boolean alreadyBlacklisted = enabledVariantBlacklistFlags[enabledVariantIndex]; - enabledVariantBlacklistFlags[enabledVariantIndex] = true; - enabledVariantBlacklistTimes[enabledVariantIndex] = SystemClock.elapsedRealtime(); - if (alreadyBlacklisted) { - // The playlist was already blacklisted. - Log.w(TAG, "Already blacklisted variant (" + responseCode + "): " - + chunk.dataSpec.uri); - return false; - } else if (!allEnabledVariantsBlacklisted()) { - // We've handled the 404/410 by blacklisting the variant. - Log.w(TAG, "Blacklisted variant (" + responseCode + "): " - + chunk.dataSpec.uri); - return true; - } else { - // This was the last non-blacklisted playlist. Don't blacklist it. - Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): " - + chunk.dataSpec.uri); - enabledVariantBlacklistFlags[enabledVariantIndex] = false; - return false; + int trackSelectionIndex = trackSelection.indexOf(trackGroup.indexOf(chunk.trackFormat)); + if (trackSelectionIndex != -1) { + boolean blacklisted = trackSelection.blacklist(trackSelectionIndex, + DEFAULT_PLAYLIST_BLACKLIST_MS); + if (blacklisted) { + // We've handled the 404/410 by blacklisting the variant. + Log.w(TAG, "Blacklisted variant (" + responseCode + "): " + chunk.dataSpec.uri); + return true; + } else { + // This was the last non-blacklisted playlist. Don't blacklist it. + Log.w(TAG, "Final variant not blacklisted (" + responseCode + "): " + + chunk.dataSpec.uri); + return false; + } } } } return false; } - public void release() { - disableFormatEvaluator(); - } - // Private methods. - private void selectTracksInternal(TrackSelection trackSelection, boolean isExternal) { - this.trackSelection = trackSelection; - seenFirstExternalTrackSelection |= isExternal; - - // Reset the enabled variant blacklist flags. - enabledVariantBlacklistTimes = new long[trackSelection.length]; - enabledVariantBlacklistFlags = new boolean[trackSelection.length]; - - if (!isExternal) { - return; - } - - disableFormatEvaluator(); - if (trackSelection.length > 1) { - Format[] formats = trackSelection.getFormats(); - formatEvaluator.enable(formats); - formatEvaluatorEnabled = true; - if (!Util.contains(formats, evaluation.format)) { - evaluation.format = null; - } - } else { - evaluation.reason = C.SELECTION_REASON_UNKNOWN; - evaluation.data = null; - } - } - - private void updateFormatEvaluation(HlsMediaChunk previous, long playbackPositionUs) { - clearStaleBlacklistedVariants(); - if (!seenFirstExternalTrackSelection) { - if (!enabledVariantBlacklistFlags[trackSelection.indexOf(variants[0].format)]) { - // Use the first variant prior to external track selection, unless it's been blacklisted. - evaluation.format = variants[0].format; - return; - } - // Try from lowest bitrate to highest. - for (int i = trackSelection.length - 1; i >= 0; i--) { - if (!enabledVariantBlacklistFlags[i]) { - evaluation.format = trackSelection.getFormat(i); - return; - } - } - // Should never happen. - throw new IllegalStateException(); - } - if (trackSelection.length == 1) { - evaluation.format = trackSelection.getFormat(0); - return; - } + private void updateSelectedTrack(HlsMediaChunk previous, long playbackPositionUs) { long bufferedDurationUs; if (previous != null) { // Use start time of the previous chunk rather than its end time because switching format @@ -500,7 +438,7 @@ public class HlsChunkSource { } else { bufferedDurationUs = 0; } - formatEvaluator.evaluateFormat(bufferedDurationUs, enabledVariantBlacklistFlags, evaluation); + trackSelection.updateSelectedTrack(bufferedDurationUs); } private boolean shouldRerequestLiveMediaPlaylist(int variantIndex) { @@ -562,34 +500,54 @@ public class HlsChunkSource { durationUs = live ? C.UNSET_TIME_US : mediaPlaylist.durationUs; } - private boolean allEnabledVariantsBlacklisted() { - for (boolean enabledVariantBlacklistFlag : enabledVariantBlacklistFlags) { - if (!enabledVariantBlacklistFlag) { - return false; - } - } - return true; - } - - private void clearStaleBlacklistedVariants() { - long currentTime = SystemClock.elapsedRealtime(); - for (int i = 0; i < enabledVariantBlacklistFlags.length; i++) { - if (enabledVariantBlacklistFlags[i] - && currentTime - enabledVariantBlacklistTimes[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) { - enabledVariantBlacklistFlags[i] = false; - } - } - } - - private void disableFormatEvaluator() { - if (formatEvaluatorEnabled) { - formatEvaluator.disable(); - formatEvaluatorEnabled = false; - } - } - // Private classes. + /** + * A {@link TrackSelection} to use for initialization. + */ + public static final class InitializationTrackSelection extends BaseTrackSelection { + + private int selectedIndex; + + public InitializationTrackSelection(TrackGroup group, int[] tracks) { + super(group, tracks); + selectedIndex = indexOf(group.getFormat(0)); + } + + @Override + public void updateSelectedTrack(long bufferedDurationUs) { + long nowMs = SystemClock.elapsedRealtime(); + if (!isBlacklisted(selectedIndex, nowMs)) { + return; + } + // Try from lowest bitrate to highest. + for (int i = length - 1; i >= 0; i--) { + if (!isBlacklisted(i, nowMs)) { + selectedIndex = i; + return; + } + } + // Should never happen. + throw new IllegalStateException(); + } + + @Override + public int getSelectedIndex() { + return selectedIndex; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Override + public Object getSelectionData() { + return null; + } + + } + private static final class MediaPlaylistChunk extends DataChunk { public final int variantIndex; diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index ae2c85acec..74e649cc9c 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.Timeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; @@ -64,7 +63,6 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private final Uri manifestUri; private final DataSource.Factory dataSourceFactory; - private final FormatEvaluator.Factory formatEvaluatorFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final IdentityHashMap sampleStreamSources; @@ -89,19 +87,17 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, Handler eventHandler, + public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, formatEvaluatorFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + eventListener); } public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory, int minLoadableRetryCount, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; this.minLoadableRetryCount = minLoadableRetryCount; eventDispatcher = new EventDispatcher(eventHandler, eventListener); @@ -352,7 +348,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, Format.NO_VALUE); Variant[] variants = new Variant[] {new Variant(playlist.baseUri, format, null)}; sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, - formatEvaluatorFactory.createFormatEvaluator(), null, null)); + null, null)); return sampleStreamWrappers; } @@ -386,8 +382,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, Variant[] variants = new Variant[selectedVariants.size()]; selectedVariants.toArray(variants); sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, - formatEvaluatorFactory.createFormatEvaluator(), masterPlaylist.muxedAudioFormat, - masterPlaylist.muxedCaptionFormat)); + masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormat)); } // Build the audio stream wrapper if applicable. @@ -396,7 +391,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, Variant[] variants = new Variant[audioVariants.size()]; audioVariants.toArray(variants); sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null, - null, null)); + null)); } // Build the text stream wrapper if applicable. @@ -405,18 +400,17 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, Variant[] variants = new Variant[subtitleVariants.size()]; subtitleVariants.toArray(variants); sampleStreamWrappers.add(buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null, - null, null)); + null)); } return sampleStreamWrappers; } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, String baseUri, - Variant[] variants, FormatEvaluator formatEvaluator, Format muxedAudioFormat, - Format muxedCaptionFormat) { + Variant[] variants, Format muxedAudioFormat, Format muxedCaptionFormat) { DataSource dataSource = dataSourceFactory.createDataSource(); HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, - timestampAdjusterProvider, formatEvaluator); + timestampAdjusterProvider); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, preparePositionUs, muxedAudioFormat, muxedCaptionFormat, minLoadableRetryCount, eventDispatcher); @@ -440,7 +434,7 @@ public final class HlsMediaSource implements MediaPeriod, MediaSource, int[] newSelectionOriginalIndices = new int[allNewSelections.size()]; for (int i = 0; i < allNewSelections.size(); i++) { TrackSelection selection = allNewSelections.get(i); - if (sampleStreamWrapperTrackGroups.indexOf(selection.group) != -1) { + if (sampleStreamWrapperTrackGroups.indexOf(selection.getTrackGroup()) != -1) { newSelectionOriginalIndices[newSelections.size()] = i; newSelections.add(selection); } diff --git a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 52865d38c7..0547231d23 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -168,11 +168,10 @@ import java.util.List; SampleStream[] newStreams = new SampleStream[newSelections.size()]; for (int i = 0; i < newStreams.length; i++) { TrackSelection selection = newSelections.get(i); - int group = trackGroups.indexOf(selection.group); - int[] tracks = selection.getTracks(); + int group = trackGroups.indexOf(selection.getTrackGroup()); setTrackGroupEnabledState(group, true); if (group == primaryTrackGroupIndex) { - chunkSource.selectTracks(new TrackSelection(chunkSource.getTrackGroup(), tracks)); + chunkSource.selectTracks(selection); } newStreams[i] = new SampleStreamImpl(group); } @@ -236,7 +235,6 @@ import java.util.List; } public void release() { - chunkSource.release(); int sampleQueueCount = sampleQueues.size(); for (int i = 0; i < sampleQueueCount; i++) { sampleQueues.valueAt(i).disable(); diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index bc2db315de..c4fcd98344 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -25,8 +25,6 @@ import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.Evaluation; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; @@ -47,24 +45,19 @@ public class DefaultSsChunkSource implements SsChunkSource { public static final class Factory implements SsChunkSource.Factory { - private final FormatEvaluator.Factory formatEvaluatorFactory; private final DataSource.Factory dataSourceFactory; - public Factory(DataSource.Factory dataSourceFactory, - FormatEvaluator.Factory formatEvaluatorFactory) { + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.formatEvaluatorFactory = formatEvaluatorFactory; } @Override public SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, int elementIndex, TrackSelection trackSelection, TrackEncryptionBox[] trackEncryptionBoxes) { - FormatEvaluator adaptiveEvaluator = trackSelection.length > 1 - ? formatEvaluatorFactory.createFormatEvaluator() : null; DataSource dataSource = dataSourceFactory.createDataSource(); return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex, - trackSelection, dataSource, adaptiveEvaluator, trackEncryptionBoxes); + trackSelection, dataSource, trackEncryptionBoxes); } } @@ -73,10 +66,7 @@ public class DefaultSsChunkSource implements SsChunkSource { private final int elementIndex; private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; - private final boolean[] adaptiveFormatBlacklistFlags; private final DataSource dataSource; - private final Evaluation evaluation; - private final FormatEvaluator adaptiveFormatEvaluator; private SsManifest manifest; private int currentManifestChunkOffset; @@ -89,26 +79,23 @@ public class DefaultSsChunkSource implements SsChunkSource { * @param elementIndex The index of the stream element in the manifest. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param adaptiveFormatEvaluator For adaptive tracks, selects from the available formats. * @param trackEncryptionBoxes Track encryption boxes for the stream. */ public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, int elementIndex, TrackSelection trackSelection, DataSource dataSource, - FormatEvaluator adaptiveFormatEvaluator, TrackEncryptionBox[] trackEncryptionBoxes) { + TrackEncryptionBox[] trackEncryptionBoxes) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.elementIndex = elementIndex; this.trackSelection = trackSelection; this.dataSource = dataSource; - this.adaptiveFormatEvaluator = adaptiveFormatEvaluator; - this.evaluation = new Evaluation(); StreamElement streamElement = manifest.streamElements[elementIndex]; - extractorWrappers = new ChunkExtractorWrapper[trackSelection.length]; - for (int i = 0; i < trackSelection.length; i++) { - int manifestTrackIndex = trackSelection.getTrack(i); - Format format = trackSelection.getFormat(i); + extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()]; + for (int i = 0; i < extractorWrappers.length; i++) { + int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); + Format format = streamElement.formats[i]; int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : -1; Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale, C.UNSET_TIME_US, manifest.durationUs, format, Track.TRANSFORMATION_NONE, @@ -118,12 +105,6 @@ public class DefaultSsChunkSource implements SsChunkSource { | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, track); extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format, false); } - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.enable(trackSelection.getFormats()); - adaptiveFormatBlacklistFlags = new boolean[trackSelection.length]; - } else { - adaptiveFormatBlacklistFlags = null; - } } @Override @@ -162,11 +143,10 @@ public class DefaultSsChunkSource implements SsChunkSource { @Override public int getPreferredQueueSize(long playbackPositionUs, List queue) { - if (fatalError != null || trackSelection.length < 2) { + if (fatalError != null || trackSelection.length() < 2) { return queue.size(); } - return adaptiveFormatEvaluator.evaluateQueueSize(playbackPositionUs, queue, - adaptiveFormatBlacklistFlags); + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); } @Override @@ -175,20 +155,8 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } - if (trackSelection.length > 1) { - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - adaptiveFormatEvaluator.evaluateFormat(bufferedDurationUs, adaptiveFormatBlacklistFlags, - evaluation); - } else { - evaluation.format = trackSelection.getFormat(0); - evaluation.reason = C.SELECTION_REASON_UNKNOWN; - evaluation.data = null; - } - - Format selectedFormat = evaluation.format; - if (selectedFormat == null) { - return; - } + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + trackSelection.updateSelectedTrack(bufferedDurationUs); StreamElement streamElement = manifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { @@ -219,14 +187,15 @@ public class DefaultSsChunkSource implements SsChunkSource { long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; - int trackSelectionIndex = trackSelection.indexOf(selectedFormat); + int trackSelectionIndex = trackSelection.getSelectedIndex(); ChunkExtractorWrapper extractorWrapper = extractorWrappers[trackSelectionIndex]; - int manifestTrackIndex = trackSelection.getTrack(trackSelectionIndex); + int manifestTrackIndex = trackSelection.getIndexInTrackGroup(trackSelectionIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); - out.chunk = newMediaChunk(selectedFormat, dataSource, uri, null, currentAbsoluteChunkIndex, - chunkStartTimeUs, chunkEndTimeUs, evaluation.reason, evaluation.data, extractorWrapper); + out.chunk = newMediaChunk(trackSelection.getSelectedFormat(), dataSource, uri, null, + currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, + trackSelection.getSelectionReason(), trackSelection.getSelectionData(), extractorWrapper); } @Override @@ -240,13 +209,6 @@ public class DefaultSsChunkSource implements SsChunkSource { return false; } - @Override - public void release() { - if (adaptiveFormatEvaluator != null) { - adaptiveFormatEvaluator.disable(); - } - } - // Private methods. private static MediaChunk newMediaChunk(Format format, DataSource dataSource, Uri uri, diff --git a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index ad4f7696db..6ca7772c05 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -199,7 +199,7 @@ import java.util.List; private ChunkSampleStream buildSampleStream(TrackSelection selection, long positionUs) { - int streamElementIndex = trackGroups.indexOf(selection.group); + int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, manifest, streamElementIndex, selection, trackEncryptionBoxes); return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, chunkSource, diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java new file mode 100644 index 0000000000..99db909ffd --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveVideoTrackSelection.java @@ -0,0 +1,252 @@ +/* + * 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 com.google.android.exoplayer2.trackselection; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.upstream.BandwidthMeter; + +import android.os.SystemClock; + +import java.util.List; + +/** + * A bandwidth based adaptive {@link TrackSelection} for video, 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 AdaptiveVideoTrackSelection extends BaseTrackSelection { + + /** + * Factory for {@link AdaptiveVideoTrackSelection} instances. + */ + public static final class Factory implements TrackSelection.Factory { + + private final BandwidthMeter bandwidthMeter; + private final int maxInitialBitrate; + private final int minDurationForQualityIncreaseMs; + private final int maxDurationForQualityDecreaseMs; + private final int minDurationToRetainAfterDiscardMs; + private final float bandwidthFraction; + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + */ + public Factory(BandwidthMeter bandwidthMeter) { + this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + 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); + } + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed + * when a bandwidth estimate is unavailable. + * @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(BandwidthMeter bandwidthMeter, int maxInitialBitrate, + int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + this.bandwidthMeter = bandwidthMeter; + this.maxInitialBitrate = maxInitialBitrate; + this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; + this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; + this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; + this.bandwidthFraction = bandwidthFraction; + } + + @Override + public AdaptiveVideoTrackSelection createTrackSelection(TrackGroup group, int... tracks) { + return new AdaptiveVideoTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, + minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, bandwidthFraction); + } + + } + + public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000; + 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.75f; + + private final BandwidthMeter bandwidthMeter; + private final int maxInitialBitrate; + private final long minDurationForQualityIncreaseUs; + private final long maxDurationForQualityDecreaseUs; + private final long minDurationToRetainAfterDiscardUs; + private final float bandwidthFraction; + + private int selectedIndex; + private int reason; + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + */ + public AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, + BandwidthMeter bandwidthMeter) { + this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + 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); + } + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a + * bandwidth estimate is unavailable. + * @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 AdaptiveVideoTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, + int maxInitialBitrate, int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, + float bandwidthFraction) { + super(group, tracks); + this.bandwidthMeter = bandwidthMeter; + this.maxInitialBitrate = maxInitialBitrate; + this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L; + this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; + this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; + this.bandwidthFraction = bandwidthFraction; + selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); + reason = C.SELECTION_REASON_INITIAL; + } + + @Override + public void updateSelectedTrack(long bufferedDurationUs) { + long nowMs = SystemClock.elapsedRealtime(); + // Get the current and ideal selections. + int currentSelectedIndex = selectedIndex; + Format currentFormat = getSelectedFormat(); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); + Format idealFormat = getFormat(idealSelectedIndex); + // Assume we can switch to the ideal selection. + selectedIndex = idealSelectedIndex; + // Revert back to the current selection if conditions are not suitable for switching. + if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) { + if (idealFormat.bitrate > currentFormat.bitrate + && bufferedDurationUs < minDurationForQualityIncreaseUs) { + // The ideal track is a higher quality, but we have insufficient buffer to safely switch + // up. Defer switching up for now. + selectedIndex = currentSelectedIndex; + } else if (idealFormat.bitrate < currentFormat.bitrate + && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { + // The ideal 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 + public Object getSelectionData() { + return null; + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + if (queue.isEmpty()) { + return 0; + } + int queueSize = queue.size(); + long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; + if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { + return queueSize; + } + int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); + Format idealFormat = getFormat(idealSelectedIndex); + // 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); + long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; + if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs + && chunk.trackFormat.bitrate < idealFormat.bitrate + && chunk.trackFormat.height < idealFormat.height + && chunk.trackFormat.height < 720 && chunk.trackFormat.width < 1280) { + return i; + } + } + return queueSize; + } + + /** + * Computes the ideal selected index ignoring buffer health. + * + * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or + * {@link Long#MIN_VALUE} to ignore blacklisting. + */ + private int determineIdealSelectedIndex(long nowMs) { + long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); + 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; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java new file mode 100644 index 0000000000..8e9303a178 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -0,0 +1,183 @@ +/* + * 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 com.google.android.exoplayer2.trackselection; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.util.Assertions; + +import android.os.SystemClock; + +import java.util.Arrays; +import java.util.List; + +/** + * An abstract base class suitable for most {@link TrackSelection} implementations. + */ +public abstract class BaseTrackSelection implements TrackSelection { + + /** + * The selected {@link TrackGroup}. + */ + protected final TrackGroup group; + /** + * The number of selected tracks within the {@link TrackGroup}. Always greater than zero. + */ + protected final int length; + + /** + * The indices of the selected tracks in {@link #group}, in order of decreasing bandwidth. + */ + private final int[] tracks; + /** + * The {@link Format}s of the selected tracks, in order of decreasing bandwidth. + */ + private final Format[] formats; + /** + * Selected track blacklist timestamps, in order of decreasing bandwidth. + */ + private final long[] blacklistUntilTimes; + + // Lazily initialized hashcode. + private int hashCode; + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + */ + public BaseTrackSelection(TrackGroup group, int... tracks) { + Assertions.checkState(tracks.length > 0); + this.group = Assertions.checkNotNull(group); + this.length = tracks.length; + // Set the formats, sorted in order of decreasing bandwidth. + formats = new Format[length]; + for (int i = 0; i < tracks.length; i++) { + formats[i] = group.getFormat(tracks[i]); + } + Arrays.sort(formats, new DecreasingBandwidthComparator()); + // Set the format indices in the same order. + this.tracks = new int[length]; + for (int i = 0; i < length; i++) { + this.tracks[i] = group.indexOf(formats[i]); + } + blacklistUntilTimes = new long[length]; + } + + @Override + public final TrackGroup getTrackGroup() { + return group; + } + + @Override + public final int length() { + return tracks.length; + } + + @Override + public final Format getFormat(int index) { + return formats[index]; + } + + @Override + public final int getIndexInTrackGroup(int index) { + return tracks[index]; + } + + @Override + public final int indexOf(Format format) { + for (int i = 0; i < length; i++) { + if (formats[i] == format) { + return i; + } + } + return -1; + } + + @Override + public final int indexOf(int indexInTrackGroup) { + for (int i = 0; i < length; i++) { + if (tracks[i] == indexInTrackGroup) { + return i; + } + } + return -1; + } + + @Override + public final Format getSelectedFormat() { + return formats[getSelectedIndex()]; + } + + @Override + public final int getSelectedIndexInTrackGroup() { + return tracks[getSelectedIndex()]; + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + return queue.size(); + } + + @Override + public final boolean blacklist(int index, long blacklistDurationMs) { + long nowMs = SystemClock.elapsedRealtime(); + boolean canBlacklist = isBlacklisted(index, nowMs); + for (int i = 0; i < length && !canBlacklist; i++) { + canBlacklist = i != index && !isBlacklisted(index, nowMs); + } + if (!canBlacklist) { + return false; + } + blacklistUntilTimes[index] = Math.max(blacklistUntilTimes[index], nowMs + blacklistDurationMs); + return true; + } + + /** + * Returns whether the track at the specified index in the selection is blaclisted. + * + * @param index The index of the track in the selection. + * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}. + */ + protected final boolean isBlacklisted(int index, long nowMs) { + return blacklistUntilTimes[index] > nowMs; + } + + // Object overrides. + + @Override + public int hashCode() { + if (hashCode == 0) { + hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks); + } + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BaseTrackSelection other = (BaseTrackSelection) obj; + return group == other.group && Arrays.equals(tracks, other.tracks); + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index e721059ae4..d95e64f02e 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -51,6 +51,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static final int[] NO_TRACKS = new int[0]; private static final String TAG = "DefaultTrackSelector"; + private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; + // Audio and text. private String preferredLanguage; @@ -64,8 +66,28 @@ public class DefaultTrackSelector extends MappingTrackSelector { private int viewportWidth; private int viewportHeight; + /** + * Constructs an instance that does not support adaptive video. + * + * @param eventHandler A handler to use when delivering events to listeners. May be null if + * listeners will not be added. + */ public DefaultTrackSelector(Handler eventHandler) { + this(eventHandler, null); + } + + /** + * Constructs an instance that uses a factory to create adaptive video track selections. + * + * @param eventHandler A handler to use when delivering events to listeners. May be null if + * listeners will not be added. + * @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, + * or null if the selector should not support adaptive video. + */ + public DefaultTrackSelector(Handler eventHandler, + TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { super(eventHandler); + this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; allowNonSeamlessAdaptiveness = true; exceedVideoConstraintsIfNecessary = true; maxVideoWidth = Integer.MAX_VALUE; @@ -203,11 +225,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { rendererTrackSelections[i] = selectTrackForVideoRenderer(rendererCapabilities[i], rendererTrackGroupArrays[i], rendererFormatSupports[i], maxVideoWidth, maxVideoHeight, allowNonSeamlessAdaptiveness, allowMixedMimeAdaptiveness, viewportWidth, - viewportHeight, orientationMayChange); - if (rendererTrackSelections[i] == null && exceedVideoConstraintsIfNecessary) { - rendererTrackSelections[i] = selectSmallestSupportedVideoTrack( - rendererTrackGroupArrays[i], rendererFormatSupports[i]); - } + viewportHeight, orientationMayChange, adaptiveVideoTrackSelectionFactory, + exceedVideoConstraintsIfNecessary); break; case C.TRACK_TYPE_AUDIO: rendererTrackSelections[i] = selectTrackForAudioRenderer(rendererTrackGroupArrays[i], @@ -232,28 +251,35 @@ public class DefaultTrackSelector extends MappingTrackSelector { RendererCapabilities rendererCapabilities, TrackGroupArray trackGroups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, int viewportWidth, - int viewportHeight, boolean orientationMayChange) throws ExoPlaybackException { - int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness - ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) - : RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness - && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; - TrackGroup largestAdaptiveGroup = null; - int[] largestAdaptiveGroupTracks = NO_TRACKS; - for (int i = 0; i < trackGroups.length; i++) { - TrackGroup trackGroup = trackGroups.get(i); - int[] adaptiveTracks = getAdaptiveTracksOfGroup(trackGroup, formatSupport[i], - allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - viewportWidth, viewportHeight, orientationMayChange); - if (adaptiveTracks.length > largestAdaptiveGroupTracks.length) { - largestAdaptiveGroup = trackGroup; - largestAdaptiveGroupTracks = adaptiveTracks; + int viewportHeight, boolean orientationMayChange, + TrackSelection.Factory adaptiveVideoTrackSelectionFactory, + boolean exceedVideoConstraintsIfNecessary) throws ExoPlaybackException { + if (adaptiveVideoTrackSelectionFactory != null) { + int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness + ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) + : RendererCapabilities.ADAPTIVE_SEAMLESS; + boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness + && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) + != 0; + TrackGroup largestAdaptiveGroup = null; + int[] largestAdaptiveGroupTracks = NO_TRACKS; + for (int i = 0; i < trackGroups.length; i++) { + TrackGroup trackGroup = trackGroups.get(i); + int[] adaptiveTracks = getAdaptiveTracksOfGroup(trackGroup, formatSupport[i], + allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, + viewportWidth, viewportHeight, orientationMayChange); + if (adaptiveTracks.length > largestAdaptiveGroupTracks.length) { + largestAdaptiveGroup = trackGroup; + largestAdaptiveGroupTracks = adaptiveTracks; + } + } + if (largestAdaptiveGroup != null) { + return adaptiveVideoTrackSelectionFactory.createTrackSelection(largestAdaptiveGroup, + largestAdaptiveGroupTracks); } } - if (largestAdaptiveGroup != null) { - return new TrackSelection(largestAdaptiveGroup, largestAdaptiveGroupTracks); - } + // TODO: Should select the best supported video track, not the first one. // No adaptive tracks selection could be made, so we select the first supported video track. for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { TrackGroup trackGroup = trackGroups.get(groupIndex); @@ -261,10 +287,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupportedVideoTrack(trackFormatSupport[trackIndex], trackGroup.getFormat(trackIndex), maxVideoWidth, maxVideoHeight)) { - return new TrackSelection(trackGroup, trackIndex); + return new FixedTrackSelection(trackGroup, trackIndex); } } } + + if (exceedVideoConstraintsIfNecessary) { + return selectSmallestSupportedVideoTrack(trackGroups, formatSupport); + } + return null; } @@ -350,8 +381,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } } - return trackGroupSelection != null - ? new TrackSelection(trackGroupSelection, trackIndexSelection) : null; + return trackGroupSelection == null ? null + : new FixedTrackSelection(trackGroupSelection, trackIndexSelection); } private static boolean isSupportedVideoTrack(int formatSupport, Format format, int maxVideoWidth, @@ -371,7 +402,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex]) && formatHasLanguage(trackGroup.getFormat(trackIndex), preferredLanguage)) { - return new TrackSelection(trackGroup, trackIndex); + return new FixedTrackSelection(trackGroup, trackIndex); } } } @@ -398,12 +429,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { firstForcedTrack = trackIndex; } if (formatHasLanguage(trackGroup.getFormat(trackIndex), preferredLanguage)) { - return new TrackSelection(trackGroup, trackIndex); + return new FixedTrackSelection(trackGroup, trackIndex); } } } } - return firstForcedGroup != null ? new TrackSelection(firstForcedGroup, firstForcedTrack) : null; + return firstForcedGroup == null ? null + : new FixedTrackSelection(firstForcedGroup, firstForcedTrack); } // General track selection methods. @@ -415,7 +447,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { if (isSupported(trackFormatSupport[trackIndex])) { - return new TrackSelection(trackGroup, trackIndex); + return new FixedTrackSelection(trackGroup, trackIndex); } } } @@ -492,7 +524,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Before API 25 the platform Display object does not provide a working way to identify Android // TVs that can show 4k resolution in a SurfaceView, so check for supported devices here. if (Util.SDK_INT < 25) { - if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL != null && Util.MODEL.startsWith("BRAVIA") + if ("Sony".equals(Util.MANUFACTURER) && Util.MODEL.startsWith("BRAVIA") && context.getPackageManager().hasSystemFeature("com.sony.dtv.hardware.panel.qfhd")) { return new Point(3840, 2160); } else if ("NVIDIA".equals(Util.MANUFACTURER) && Util.MODEL != null diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java new file mode 100644 index 0000000000..9cf4f0371b --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -0,0 +1,69 @@ +/* + * 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 com.google.android.exoplayer2.trackselection; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.TrackGroup; + +/** + * A {@link TrackSelection} consisting of a single track. + */ +public final class FixedTrackSelection extends BaseTrackSelection { + + private final int reason; + private final Object data; + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param track The index of the selected track within the {@link TrackGroup}. + */ + public FixedTrackSelection(TrackGroup group, int track) { + this(group, track, C.SELECTION_REASON_UNKNOWN, null); + } + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param track The index of the selected track within the {@link TrackGroup}. + * @param reason A reason for the track selection. + * @param data Optional data associated with the track selection. + */ + public FixedTrackSelection(TrackGroup group, int track, int reason, Object data) { + super(group, track); + this.reason = reason; + this.data = data; + } + + @Override + public void updateSelectedTrack(long bufferedDurationUs) { + // Do nothing. + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return reason; + } + + @Override + public Object getSelectionData() { + return data; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java new file mode 100644 index 0000000000..a4ba2cae8c --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -0,0 +1,96 @@ +/* + * 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 com.google.android.exoplayer2.trackselection; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.TrackGroup; + +import android.os.SystemClock; + +import java.util.Random; + +/** + * A {@link TrackSelection} whose selected track is updated randomly. + */ +public final class RandomTrackSelection extends BaseTrackSelection { + + private final Random random; + + private int selectedIndex; + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + */ + public RandomTrackSelection(TrackGroup group, int... tracks) { + super(group, tracks); + random = new Random(); + selectedIndex = random.nextInt(length); + } + + /** + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + * @param seed A seed for the {@link Random} instance used to update the selected track. + */ + public RandomTrackSelection(TrackGroup group, int[] tracks, long seed) { + super(group, tracks); + random = new Random(seed); + selectedIndex = random.nextInt(length); + } + + @Override + public void updateSelectedTrack(long bufferedDurationUs) { + // Count the number of non-blacklisted formats. + long nowMs = SystemClock.elapsedRealtime(); + int nonBlacklistedFormatCount = 0; + for (int i = 0; i < length; i++) { + if (!isBlacklisted(i, nowMs)) { + nonBlacklistedFormatCount++; + } + } + + selectedIndex = random.nextInt(nonBlacklistedFormatCount); + if (nonBlacklistedFormatCount != length) { + // Adjust the format index to account for blacklisted formats. + nonBlacklistedFormatCount = 0; + for (int i = 0; i < length; i++) { + if (!isBlacklisted(i, nowMs) && selectedIndex == nonBlacklistedFormatCount++) { + selectedIndex = i; + return; + } + } + } + } + + @Override + public int getSelectedIndex() { + return selectedIndex; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_ADAPTIVE; + } + + @Override + public Object getSelectionData() { + return null; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 05fc6061c6..edf8d7dfee 100644 --- a/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -16,71 +16,64 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Format.DecreasingBandwidthComparator; import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.source.chunk.MediaChunk; -import java.util.Arrays; +import java.util.List; /** - * A track selection, consisting of a {@link TrackGroup} and a selected subset of the tracks within - * it. The selected tracks are exposed in order of decreasing bandwidth. + * A track selection consisting of a static subset of selected tracks belonging to a + * {@link TrackGroup}, and a possibly varying individual selected track from the subset. + *

+ * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected + * track may change as a result of calling {@link #updateSelectedTrack(long)}. */ -public final class TrackSelection { +public interface TrackSelection { /** - * The selected {@link TrackGroup}. + * Factory for {@link TrackSelection} instances. */ - public final TrackGroup group; - /** - * The number of selected tracks within the {@link TrackGroup}. Always greater than zero. - */ - public final int length; + interface Factory { - private final int[] tracks; - private final Format[] formats; + /** + * Creates a new selection. + * + * @param group The {@link TrackGroup}. Must not be null. + * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be + * null or empty. May be in any order. + * @return The created selection. + */ + TrackSelection createTrackSelection(TrackGroup group, int... tracks); - // Lazily initialized hashcode. - private int hashCode; - - /** - * @param group The {@link TrackGroup}. Must not be null. - * @param tracks The indices of the selected tracks within the {@link TrackGroup}. Must not be - * null or empty. May be in any order. - */ - public TrackSelection(TrackGroup group, int... tracks) { - Assertions.checkState(tracks.length > 0); - this.group = Assertions.checkNotNull(group); - this.length = tracks.length; - // Set the formats, sorted in order of decreasing bandwidth. - formats = new Format[length]; - for (int i = 0; i < tracks.length; i++) { - formats[i] = group.getFormat(tracks[i]); - } - Arrays.sort(formats, new DecreasingBandwidthComparator()); - // Set the format indices in the same order. - this.tracks = new int[length]; - for (int i = 0; i < length; i++) { - this.tracks[i] = group.indexOf(formats[i]); - } } + /** + * Returns the {@link TrackGroup} to which the selected tracks belong. + */ + TrackGroup getTrackGroup(); + + // Static subset of selected tracks. + + /** + * Returns the number of tracks in the selection. + */ + int length(); + /** * Returns the format of the track at a given index in the selection. * * @param index The index in the selection. * @return The format of the selected track. */ - public Format getFormat(int index) { - return formats[index]; - } + Format getFormat(int index); /** - * Returns a copy of the formats of the selected tracks. + * Returns the index in the track group of the track at a given index in the selection. + * + * @param index The index in the selection. + * @return The index of the selected track. */ - public Format[] getFormats() { - return formats.clone(); - } + int getIndexInTrackGroup(int index); /** * Returns the index in the selection of the track with the specified format. @@ -89,66 +82,82 @@ public final class TrackSelection { * @return The index in the selection, or -1 if the track with the specified format is not part of * the selection. */ - public int indexOf(Format format) { - for (int i = 0; i < length; i++) { - if (formats[i] == format) { - return i; - } - } - return -1; - } - - /** - * Returns the index in the track group of the track at a given index in the selection. - * - * @param index The index in the selection. - * @return The index of the selected track. - */ - public int getTrack(int index) { - return tracks[index]; - } - - /** - * Returns a copy of the selected tracks in the track group. - */ - public int[] getTracks() { - return tracks.clone(); - } + int indexOf(Format format); /** * Returns the index in the selection of the track with the specified index in the track group. * - * @param trackIndex The index in the track group. + * @param indexInTrackGroup The index in the track group. * @return The index in the selection, or -1 if the track with the specified index is not part of * the selection. */ - public int indexOf(int trackIndex) { - for (int i = 0; i < length; i++) { - if (tracks[i] == trackIndex) { - return i; - } - } - return -1; - } + int indexOf(int indexInTrackGroup); - @Override - public int hashCode() { - if (hashCode == 0) { - hashCode = 31 * System.identityHashCode(group) + Arrays.hashCode(tracks); - } - return hashCode; - } + // Individual selected track. - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - TrackSelection other = (TrackSelection) obj; - return group == other.group && Arrays.equals(tracks, other.tracks); - } + /** + * Returns the {@link Format} of the individual selected track. + */ + Format getSelectedFormat(); + + /** + * Returns the index in the track group of the individual selected track. + */ + int getSelectedIndexInTrackGroup(); + + /** + * Returns the index of the selected track. + */ + int getSelectedIndex(); + + /** + * Returns the reason for the current track selection. + */ + int getSelectionReason(); + + /** + * Returns optional data associated with the current track selection. + */ + Object getSelectionData(); + + // Adaptation. + + /** + * Updates the selected track. + * + * @param bufferedDurationUs The duration of media currently buffered in microseconds. + */ + void updateSelectedTrack(long bufferedDurationUs); + + /** + * May be called periodically by sources that load media in discrete {@link MediaChunk}s and + * support discarding of buffered chunks in order to re-buffer using a different selected track. + * Returns the number of chunks that should be retained in the queue. + *

+ * To avoid excessive re-buffering, implementations should normally return the size of the queue. + * An example of a case where a smaller value may be returned is if network conditions have + * improved dramatically, allowing chunks to be discarded and re-buffered in a track of + * significantly higher quality. Discarding chunks may allow faster switching to a higher quality + * track in this case. + * + * @param playbackPositionUs The current playback position in microseconds. + * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified. + * @return The number of chunks to retain in the queue. + */ + int evaluateQueueSize(long playbackPositionUs, List queue); + + /** + * Attempts to blacklist the track at the specified index in the selection, making it ineligible + * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time. + * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the + * currently selected track, note that it will remain selected until the next call to + * {@link #updateSelectedTrack(long)}. + * + * @param index The index of the track in the selection. + * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in + * milliseconds. + * @return Whether blacklisting was successful. + */ + boolean blacklist(int index, long blacklistDurationMs); } diff --git a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java index 7a63b8d393..446f4b6281 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer2/playbacktests/gts/DashTest.java @@ -34,17 +34,18 @@ import com.google.android.exoplayer2.playbacktests.util.MetricsLogger; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator; -import com.google.android.exoplayer2.source.chunk.FormatEvaluator.AdaptiveEvaluator; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.RandomTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -680,15 +681,16 @@ public final class DashTest extends ActivityInstrumentationTestCase2 videoFormatIds.length; + selections[VIDEO_RENDERER_INDEX].length() > videoFormatIds.length; return selections; } @@ -844,18 +844,9 @@ public final class DashTest extends ActivityInstrumentationTestCase2 trackIndices = new ArrayList<>(); - // Always select explicitly listed representations, failing if they're missing. + // Always select explicitly listed representations. for (String formatId : formatIds) { - boolean foundIndex = false; - for (int j = 0; j < trackGroup.length && !foundIndex; j++) { - if (trackGroup.getFormat(j).id.equals(formatId)) { - trackIndices.add(j); - foundIndex = true; - } - } - if (!foundIndex) { - throw new IllegalStateException("Format " + formatId + " not found."); - } + trackIndices.add(getTrackIndex(trackGroup, formatId)); } // Select additional video representations, if supported by the device. @@ -873,6 +864,15 @@ public final class DashTest extends ActivityInstrumentationTestCase2