diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 816af9f9a2..acd0353c7a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -38,6 +38,8 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import com.google.common.collect.ComparisonChain; +import com.google.common.collect.Ordering; import com.google.common.primitives.Ints; import java.util.ArrayList; import java.util.Arrays; @@ -1488,12 +1490,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ private static final float FRACTION_TO_CONSIDER_FULLSCREEN = 0.98f; private static final int[] NO_TRACKS = new int[0]; - - // Constants that are added to a track's score when certain conditions are met. Higher scoring - // tracks are selected over those with lower scores. - private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000; - private static final int SATISFIES_MIN_VIDEO_CONSTRAINTS_BONUS = 1; - private static final int SATISFIES_MAX_VIDEO_CONSTRAINTS_BONUS = 2; + /** Ordering of two format values. A known value is considered greater than Format#NO_VALUE. */ + private static final Ordering FORMAT_VALUE_ORDERING = + Ordering.from( + (first, second) -> + first == Format.NO_VALUE + ? (second == Format.NO_VALUE ? 0 : -1) + : (second == Format.NO_VALUE ? 1 : (first - second))); + /** Ordering where all elements are equal. */ + private static final Ordering NO_ORDER = Ordering.from((first, second) -> 0); private final TrackSelection.Factory trackSelectionFactory; private final AtomicReference parametersReference; @@ -1690,13 +1695,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - AudioTrackScore selectedAudioTrackScore = null; - String selectedAudioLanguage = null; + @Nullable AudioTrackScore selectedAudioTrackScore = null; + @Nullable String selectedAudioLanguage = null; int selectedAudioRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { if (C.TRACK_TYPE_AUDIO == mappedTrackInfo.getRendererType(i)) { boolean enableAdaptiveTrackSelection = allowMultipleAdaptiveSelections || !seenVideoRendererWithMappedTracks; + @Nullable Pair audioSelection = selectAudioTrack( mappedTrackInfo.getTrackGroups(i), @@ -1722,7 +1728,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - TextTrackScore selectedTextTrackScore = null; + @Nullable TextTrackScore selectedTextTrackScore = null; int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { int trackType = mappedTrackInfo.getRendererType(i); @@ -1732,6 +1738,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Already done. Do nothing. break; case C.TRACK_TYPE_TEXT: + @Nullable Pair textSelection = selectTextTrack( mappedTrackInfo.getTrackGroups(i), @@ -2019,15 +2026,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Nullable private static TrackSelection.Definition selectFixedVideoTrack( TrackGroupArray groups, @Capabilities int[][] formatSupports, Parameters params) { + int selectedTrackIndex = C.INDEX_UNSET; @Nullable TrackGroup selectedGroup = null; - int selectedTrackIndex = 0; - int selectedTrackScore = 0; - int selectedBitrate = Format.NO_VALUE; - int selectedPixelCount = Format.NO_VALUE; + @Nullable VideoTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); - List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, - params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); + List viewportFilteredTrackIndices = + getViewportFilteredTrackIndices( + trackGroup, + params.viewportWidth, + params.viewportHeight, + params.viewportOrientationMayChange); @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { Format format = trackGroup.getFormat(trackIndex); @@ -2037,82 +2046,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { } if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { - boolean satisfiesMaxConstraints = - selectedTrackIndices.contains(trackIndex) - && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight) - && (format.frameRate == Format.NO_VALUE - || format.frameRate <= params.maxVideoFrameRate) - && (format.bitrate == Format.NO_VALUE - || format.bitrate <= params.maxVideoBitrate); - boolean satisfiesMinConstraints = - selectedTrackIndices.contains(trackIndex) - && (format.width == Format.NO_VALUE || format.width >= params.minVideoWidth) - && (format.height == Format.NO_VALUE || format.height >= params.minVideoHeight) - && (format.frameRate == Format.NO_VALUE - || format.frameRate >= params.minVideoFrameRate) - && (format.bitrate == Format.NO_VALUE - || format.bitrate >= params.minVideoBitrate); - if (!satisfiesMaxConstraints && !params.exceedVideoConstraintsIfNecessary) { + VideoTrackScore trackScore = + new VideoTrackScore( + format, + params, + trackFormatSupport[trackIndex], + viewportFilteredTrackIndices.contains(trackIndex)); + if (!trackScore.isWithinMaxConstraints && !params.exceedVideoConstraintsIfNecessary) { // Track should not be selected. continue; } - int trackScore = 1; - boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false); - if (isWithinCapabilities) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (satisfiesMaxConstraints) { - trackScore += SATISFIES_MAX_VIDEO_CONSTRAINTS_BONUS; - } - if (satisfiesMinConstraints) { - trackScore += SATISFIES_MIN_VIDEO_CONSTRAINTS_BONUS; - } - boolean selectTrack = trackScore > selectedTrackScore; - if (trackScore == selectedTrackScore) { - // The current selection and the track being evaluated have the same score. Apply - // tiebreaking logic to choose between them. - int bitrateComparison = compareFormatValues(format.bitrate, selectedBitrate); - if (params.forceLowestBitrate && bitrateComparison != 0) { - // Use bitrate as a tiebreaker, preferring the lower bitrate. - selectTrack = bitrateComparison < 0; - } else { - // Use pixel count as a tiebreaker, followed by bitrate if pixel counts are equal. - int formatPixelCount = format.getPixelCount(); - int comparisonResult = formatPixelCount != selectedPixelCount - ? compareFormatValues(formatPixelCount, selectedPixelCount) - : compareFormatValues(format.bitrate, selectedBitrate); - if (!isWithinCapabilities) { - // We're not within capabilities, so pick the lower quality because it's more likely - // to play successfully. - selectTrack = comparisonResult < 0; - } else if (satisfiesMinConstraints && satisfiesMaxConstraints) { - // All constraints are satisfied, so pick the higher quality. - selectTrack = comparisonResult > 0; - } else if (!satisfiesMinConstraints && satisfiesMaxConstraints) { - // Pick the higher quality because it gets us closest to satisfying the violated min - // constraints. - selectTrack = comparisonResult > 0; - } else if (satisfiesMinConstraints) { // !satisfiesMaxConstraints - // Pick the lower quality because it gets us closest to satisfying the violated max - // constraints. - selectTrack = comparisonResult > 0; - } else { // !satisfiesMinConstraints && !satisfiesMaxConstraints - // Arbitrarily pick the lower quality. - selectTrack = comparisonResult < 0; - } - } - } - if (selectTrack) { + if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; - selectedBitrate = format.bitrate; - selectedPixelCount = format.getPixelCount(); } } } } + return selectedGroup == null ? null : new TrackSelection.Definition(selectedGroup, selectedTrackIndex); @@ -2146,7 +2098,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { throws ExoPlaybackException { int selectedTrackIndex = C.INDEX_UNSET; int selectedGroupIndex = C.INDEX_UNSET; - AudioTrackScore selectedTrackScore = null; + @Nullable AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); @Capabilities int[] trackFormatSupport = formatSupports[groupIndex]; @@ -2272,9 +2224,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { Parameters params, @Nullable String selectedAudioLanguage) throws ExoPlaybackException { - TrackGroup selectedGroup = null; + @Nullable TrackGroup selectedGroup = null; int selectedTrackIndex = C.INDEX_UNSET; - TextTrackScore selectedTrackScore = null; + @Nullable TextTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; @@ -2319,9 +2271,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected TrackSelection.Definition selectOtherTrack( int trackType, TrackGroupArray groups, @Capabilities int[][] formatSupport, Parameters params) throws ExoPlaybackException { - TrackGroup selectedGroup = null; + @Nullable TrackGroup selectedGroup = null; int selectedTrackIndex = 0; - int selectedTrackScore = 0; + @Nullable OtherTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); @Capabilities int[] trackFormatSupport = formatSupport[groupIndex]; @@ -2329,12 +2281,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - int trackScore = isDefault ? 2 : 1; - if (isSupported(trackFormatSupport[trackIndex], false)) { - trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; - } - if (trackScore > selectedTrackScore) { + OtherTrackScore trackScore = new OtherTrackScore(format, trackFormatSupport[trackIndex]); + if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroup = trackGroup; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; @@ -2437,21 +2385,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { return true; } - /** - * Compares two format values for order. A known value is considered greater than {@link - * Format#NO_VALUE}. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareFormatValues(int first, int second) { - return first == Format.NO_VALUE - ? (second == Format.NO_VALUE ? 0 : -1) - : (second == Format.NO_VALUE ? 1 : (first - second)); - } - /** * Returns true if the {@link FormatSupport} in the given {@link Capabilities} is {@link * RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set and the @@ -2590,16 +2523,75 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - /** - * Compares two integers in a safe way avoiding potential overflow. - * - * @param first The first value. - * @param second The second value. - * @return A negative integer if the first value is less than the second. Zero if they are equal. - * A positive integer if the first value is greater than the second. - */ - private static int compareInts(int first, int second) { - return first > second ? 1 : (second > first ? -1 : 0); + /** Represents how well a video track matches the selection {@link Parameters}. */ + protected static final class VideoTrackScore implements Comparable { + + /** + * Whether the provided format is within the parameter maximum constraints. If {@code false}, + * the format should not be selected. + */ + public final boolean isWithinMaxConstraints; + + private final Parameters parameters; + private final boolean isWithinMinConstraints; + private final boolean isWithinRendererCapabilities; + private final int bitrate; + private final int pixelCount; + + public VideoTrackScore( + Format format, + Parameters parameters, + @Capabilities int formatSupport, + boolean isSuitableForViewport) { + this.parameters = parameters; + isWithinMaxConstraints = + isSuitableForViewport + && (format.width == Format.NO_VALUE || format.width <= parameters.maxVideoWidth) + && (format.height == Format.NO_VALUE || format.height <= parameters.maxVideoHeight) + && (format.frameRate == Format.NO_VALUE + || format.frameRate <= parameters.maxVideoFrameRate) + && (format.bitrate == Format.NO_VALUE + || format.bitrate <= parameters.maxVideoBitrate); + isWithinMinConstraints = + isSuitableForViewport + && (format.width == Format.NO_VALUE || format.width >= parameters.minVideoWidth) + && (format.height == Format.NO_VALUE || format.height >= parameters.minVideoHeight) + && (format.frameRate == Format.NO_VALUE + || format.frameRate >= parameters.minVideoFrameRate) + && (format.bitrate == Format.NO_VALUE + || format.bitrate >= parameters.minVideoBitrate); + isWithinRendererCapabilities = + isSupported(formatSupport, /* allowExceedsCapabilities= */ false); + bitrate = format.bitrate; + pixelCount = format.getPixelCount(); + } + + @Override + public int compareTo(VideoTrackScore other) { + // The preferred ordering by video quality depends on the constraints: + // - Not within renderer capabilities: Prefer lower quality because it's more likely to play. + // - Within min and max constraints: Prefer higher quality. + // - Within max constraints only: Prefer higher quality because it gets us closest to + // satisfying the violated min constraints. + // - Within min constraints only: Prefer lower quality because it gets us closest to + // satisfying the violated max constraints. + // - Outside min and max constraints: Arbitrarily prefer lower quality. + Ordering qualityOrdering = + isWithinMaxConstraints && isWithinRendererCapabilities + ? FORMAT_VALUE_ORDERING + : FORMAT_VALUE_ORDERING.reverse(); + return ComparisonChain.start() + .compareFalseFirst(this.isWithinRendererCapabilities, other.isWithinRendererCapabilities) + .compareFalseFirst(this.isWithinMaxConstraints, other.isWithinMaxConstraints) + .compareFalseFirst(this.isWithinMinConstraints, other.isWithinMinConstraints) + .compare( + this.bitrate, + other.bitrate, + parameters.forceLowestBitrate ? FORMAT_VALUE_ORDERING.reverse() : NO_ORDER) + .compare(this.pixelCount, other.pixelCount, qualityOrdering) + .compare(this.bitrate, other.bitrate, qualityOrdering) + .result(); + } } /** Represents how well an audio track matches the selection {@link Parameters}. */ @@ -2625,7 +2617,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { public AudioTrackScore(Format format, Parameters parameters, @Capabilities int formatSupport) { this.parameters = parameters; this.language = normalizeUndeterminedLanguageToNull(format.language); - isWithinRendererCapabilities = isSupported(formatSupport, false); + isWithinRendererCapabilities = + isSupported(formatSupport, /* allowExceedsCapabilities= */ false); preferredLanguageScore = getFormatLanguageScore( format, @@ -2665,44 +2658,34 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(AudioTrackScore other) { - if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { - return this.isWithinRendererCapabilities ? 1 : -1; - } - if (this.preferredLanguageScore != other.preferredLanguageScore) { - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); - } - if (this.isWithinConstraints != other.isWithinConstraints) { - return this.isWithinConstraints ? 1 : -1; - } - if (parameters.forceLowestBitrate) { - int bitrateComparison = compareFormatValues(bitrate, other.bitrate); - if (bitrateComparison != 0) { - return bitrateComparison > 0 ? -1 : 1; - } - } - if (this.isDefaultSelectionFlag != other.isDefaultSelectionFlag) { - return this.isDefaultSelectionFlag ? 1 : -1; - } - if (this.localeLanguageMatchIndex != other.localeLanguageMatchIndex) { - return -compareInts(this.localeLanguageMatchIndex, other.localeLanguageMatchIndex); - } - if (this.localeLanguageScore != other.localeLanguageScore) { - return compareInts(this.localeLanguageScore, other.localeLanguageScore); - } // If the formats are within constraints and renderer capabilities then prefer higher values // of channel count, sample rate and bit rate in that order. Otherwise, prefer lower values. - int resultSign = isWithinConstraints && isWithinRendererCapabilities ? 1 : -1; - if (this.channelCount != other.channelCount) { - return resultSign * compareInts(this.channelCount, other.channelCount); - } - if (this.sampleRate != other.sampleRate) { - return resultSign * compareInts(this.sampleRate, other.sampleRate); - } - if (Util.areEqual(this.language, other.language)) { - // Only compare bit rates of tracks with the same or unknown language. - return resultSign * compareInts(this.bitrate, other.bitrate); - } - return 0; + Ordering qualityOrdering = + isWithinConstraints && isWithinRendererCapabilities + ? FORMAT_VALUE_ORDERING + : FORMAT_VALUE_ORDERING.reverse(); + return ComparisonChain.start() + .compareFalseFirst(this.isWithinRendererCapabilities, other.isWithinRendererCapabilities) + .compare(this.preferredLanguageScore, other.preferredLanguageScore) + .compareFalseFirst(this.isWithinConstraints, other.isWithinConstraints) + .compare( + this.bitrate, + other.bitrate, + parameters.forceLowestBitrate ? FORMAT_VALUE_ORDERING.reverse() : NO_ORDER) + .compareFalseFirst(this.isDefaultSelectionFlag, other.isDefaultSelectionFlag) + .compare( + this.localeLanguageMatchIndex, + other.localeLanguageMatchIndex, + Ordering.natural().reverse()) + .compare(this.localeLanguageScore, other.localeLanguageScore) + .compare(this.channelCount, other.channelCount, qualityOrdering) + .compare(this.sampleRate, other.sampleRate, qualityOrdering) + .compare( + this.bitrate, + other.bitrate, + // Only compare bit rates of tracks with matching language information. + Util.areEqual(this.language, other.language) ? qualityOrdering : NO_ORDER) + .result(); } } @@ -2717,7 +2700,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final boolean isWithinRendererCapabilities; private final boolean isDefault; - private final boolean hasPreferredIsForcedFlag; + private final boolean isForced; private final int preferredLanguageScore; private final int preferredRoleFlagsScore; private final int selectedAudioLanguageScore; @@ -2733,7 +2716,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { int maskedSelectionFlags = format.selectionFlags & ~parameters.disabledTextTrackSelectionFlags; isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; + isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; preferredLanguageScore = getFormatLanguageScore( format, parameters.preferredTextLanguage, parameters.selectUndeterminedTextLanguage); @@ -2741,11 +2724,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { Integer.bitCount(format.roleFlags & parameters.preferredTextRoleFlags); hasCaptionRoleFlags = (format.roleFlags & (C.ROLE_FLAG_CAPTION | C.ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND)) != 0; - // Prefer non-forced to forced if a preferred text language has been matched. Where both are - // provided the non-forced track will usually contain the forced subtitles as a subset. - // Otherwise, prefer a forced track. - hasPreferredIsForcedFlag = - (preferredLanguageScore > 0 && !isForced) || (preferredLanguageScore == 0 && isForced); boolean selectedAudioLanguageUndetermined = normalizeUndeterminedLanguageToNull(selectedAudioLanguage) == null; selectedAudioLanguageScore = @@ -2766,28 +2744,49 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ @Override public int compareTo(TextTrackScore other) { - if (this.isWithinRendererCapabilities != other.isWithinRendererCapabilities) { - return this.isWithinRendererCapabilities ? 1 : -1; + ComparisonChain chain = + ComparisonChain.start() + .compareFalseFirst( + this.isWithinRendererCapabilities, other.isWithinRendererCapabilities) + .compare(this.preferredLanguageScore, other.preferredLanguageScore) + .compare(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore) + .compareFalseFirst(this.isDefault, other.isDefault) + .compare( + this.isForced, + other.isForced, + // Prefer non-forced to forced if a preferred text language has been matched. + // Where both are provided the non-forced track will usually contain the forced + // subtitles as a subset. Otherwise, prefer a forced track. + preferredLanguageScore == 0 ? Ordering.natural() : Ordering.natural().reverse()) + .compare(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); + if (preferredRoleFlagsScore == 0) { + chain = chain.compareTrueFirst(this.hasCaptionRoleFlags, other.hasCaptionRoleFlags); } - if (this.preferredLanguageScore != other.preferredLanguageScore) { - return compareInts(this.preferredLanguageScore, other.preferredLanguageScore); - } - if (this.preferredRoleFlagsScore != other.preferredRoleFlagsScore) { - return compareInts(this.preferredRoleFlagsScore, other.preferredRoleFlagsScore); - } - if (this.isDefault != other.isDefault) { - return this.isDefault ? 1 : -1; - } - if (this.hasPreferredIsForcedFlag != other.hasPreferredIsForcedFlag) { - return this.hasPreferredIsForcedFlag ? 1 : -1; - } - if (this.selectedAudioLanguageScore != other.selectedAudioLanguageScore) { - return compareInts(this.selectedAudioLanguageScore, other.selectedAudioLanguageScore); - } - if (preferredRoleFlagsScore == 0 && this.hasCaptionRoleFlags != other.hasCaptionRoleFlags) { - return this.hasCaptionRoleFlags ? -1 : 1; - } - return 0; + return chain.result(); + } + } + + /** + * Represents how well any other track (non video, audio or text) matches the selection {@link + * Parameters}. + */ + protected static final class OtherTrackScore implements Comparable { + + private final boolean isDefault; + private final boolean isWithinRendererCapabilities; + + public OtherTrackScore(Format format, @Capabilities int trackFormatSupport) { + isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + isWithinRendererCapabilities = + isSupported(trackFormatSupport, /* allowExceedsCapabilities= */ false); + } + + @Override + public int compareTo(OtherTrackScore other) { + return ComparisonChain.start() + .compareFalseFirst(this.isWithinRendererCapabilities, other.isWithinRendererCapabilities) + .compareFalseFirst(this.isDefault, other.isDefault) + .result(); } } }