diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9500d3cfef..3c611be784 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -124,6 +124,9 @@ * Fix bugs reporting events for multi-period media sources ([#4492](https://github.com/google/ExoPlayer/issues/4492) and [#4634](https://github.com/google/ExoPlayer/issues/4634)). +* Fix issue where the preferred audio or text track would not be selected if + mapped onto a secondary renderer of the corresponding type + ([#4711](http://github.com/google/ExoPlayer/issues/4711)). * Fix issue where errors of upcoming playlist items are thrown too early ([#4661](https://github.com/google/ExoPlayer/issues/4661)). * Allow edit lists which do not start with a sync sample. 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 a5c4123b10..599020e5b3 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 @@ -1318,8 +1318,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - boolean selectedAudioTracks = false; - boolean selectedTextTracks = false; + AudioTrackScore selectedAudioTrackScore = null; + int selectedAudioRendererIndex = C.INDEX_UNSET; + int selectedTextTrackScore = Integer.MIN_VALUE; + int selectedTextRendererIndex = C.INDEX_UNSET; for (int i = 0; i < rendererCount; i++) { int trackType = mappedTrackInfo.getRendererType(i); switch (trackType) { @@ -1327,23 +1329,38 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: - if (!selectedAudioTracks) { - rendererTrackSelections[i] = - selectAudioTrack( - mappedTrackInfo.getTrackGroups(i), - rendererFormatSupports[i], - rendererMixedMimeTypeAdaptationSupports[i], - params, - seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); - selectedAudioTracks = rendererTrackSelections[i] != null; + Pair audioSelection = + selectAudioTrack( + mappedTrackInfo.getTrackGroups(i), + rendererFormatSupports[i], + rendererMixedMimeTypeAdaptationSupports[i], + params, + seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); + if (audioSelection != null + && (selectedAudioTrackScore == null + || audioSelection.second.compareTo(selectedAudioTrackScore) > 0)) { + if (selectedAudioRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another audio renderer, but it had a lower + // score. Clear the selection for that renderer. + rendererTrackSelections[selectedAudioRendererIndex] = null; + } + rendererTrackSelections[i] = audioSelection.first; + selectedAudioTrackScore = audioSelection.second; + selectedAudioRendererIndex = i; } break; case C.TRACK_TYPE_TEXT: - if (!selectedTextTracks) { - rendererTrackSelections[i] = - selectTextTrack( - mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); - selectedTextTracks = rendererTrackSelections[i] != null; + Pair textSelection = + selectTextTrack(mappedTrackInfo.getTrackGroups(i), rendererFormatSupports[i], params); + if (textSelection != null && textSelection.second > selectedTextTrackScore) { + if (selectedTextRendererIndex != C.INDEX_UNSET) { + // We've already made a selection for another text renderer, but it had a lower score. + // Clear the selection for that renderer. + rendererTrackSelections[selectedTextRendererIndex] = null; + } + rendererTrackSelections[i] = textSelection.first; + selectedTextTrackScore = textSelection.second; + selectedTextRendererIndex = i; } break; default: @@ -1599,10 +1616,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param params The selector's current constraint parameters. * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or * null if a fixed track selection is required. - * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @return The {@link TrackSelection} and corresponding {@link AudioTrackScore}, or null if no + * selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected @Nullable TrackSelection selectAudioTrack( + protected @Nullable Pair selectAudioTrack( TrackGroupArray groups, int[][] formatSupports, int mixedMimeTypeAdaptationSupports, @@ -1635,6 +1653,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } TrackGroup selectedGroup = groups.get(selectedGroupIndex); + + TrackSelection selection = null; if (!params.forceHighestSupportedBitrate && !params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { @@ -1643,11 +1663,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { getAdaptiveAudioTracks( selectedGroup, formatSupports[selectedGroupIndex], params.allowMixedMimeAdaptiveness); if (adaptiveTracks.length > 0) { - return adaptiveTrackSelectionFactory - .createTrackSelection(selectedGroup, getBandwidthMeter(), adaptiveTracks); + selection = + adaptiveTrackSelectionFactory.createTrackSelection( + selectedGroup, getBandwidthMeter(), adaptiveTracks); } } - return new FixedTrackSelection(selectedGroup, selectedTrackIndex); + if (selection == null) { + // We didn't make an adaptive selection, so make a fixed one instead. + selection = new FixedTrackSelection(selectedGroup, selectedTrackIndex); + } + + return Pair.create(selection, Assertions.checkNotNull(selectedTrackScore)); } private static int[] getAdaptiveAudioTracks(TrackGroup group, int[] formatSupport, @@ -1712,10 +1738,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped * track, indexed by track group index and track index (in that order). * @param params The selector's current constraint parameters. - * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @return The {@link TrackSelection} and corresponding track score, or null if no selection was + * made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - protected @Nullable TrackSelection selectTextTrack( + protected @Nullable Pair selectTextTrack( TrackGroupArray groups, int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; @@ -1770,8 +1797,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } } - return selectedGroup == null ? null - : new FixedTrackSelection(selectedGroup, selectedTrackIndex); + return selectedGroup == null + ? null + : Pair.create( + new FixedTrackSelection(selectedGroup, selectedTrackIndex), selectedTrackScore); } // General track selection methods. @@ -2032,12 +2061,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - /** - * A representation of how well a track fits with our track selection {@link Parameters}. - * - *

This is used to rank different audio tracks relatively with each other. - */ + /** Represents how well an audio track matches the selection {@link Parameters}. */ private static final class AudioTrackScore implements Comparable { + private final Parameters parameters; private final int withinRendererCapabilitiesScore; private final int matchLanguageScore; @@ -2057,7 +2083,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Compares the score of the current track format with another {@link AudioTrackScore}. + * Compares this score with another. * * @param other The other score to compare to. * @return A positive integer if this score is better than the other. Zero if they are equal. A @@ -2086,35 +2112,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { return resultSign * compareInts(this.bitrate, other.bitrate); } } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - AudioTrackScore that = (AudioTrackScore) o; - - return withinRendererCapabilitiesScore == that.withinRendererCapabilitiesScore - && matchLanguageScore == that.matchLanguageScore - && defaultSelectionFlagScore == that.defaultSelectionFlagScore - && channelCount == that.channelCount && sampleRate == that.sampleRate - && bitrate == that.bitrate; - } - - @Override - public int hashCode() { - int result = withinRendererCapabilitiesScore; - result = 31 * result + matchLanguageScore; - result = 31 * result + defaultSelectionFlagScore; - result = 31 * result + channelCount; - result = 31 * result + sampleRate; - result = 31 * result + bitrate; - return result; - } } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 86d810989f..b650b2ece4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES; import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED; +import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE; import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.any; @@ -76,31 +77,8 @@ public final class DefaultTrackSelectorTest { private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = new RendererCapabilities[] {VIDEO_CAPABILITIES, NO_SAMPLE_CAPABILITIES}; - private static final Format VIDEO_FORMAT = - Format.createVideoSampleFormat( - "video", - MimeTypes.VIDEO_H264, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 1024, - 768, - Format.NO_VALUE, - null, - null); - private static final Format AUDIO_FORMAT = - Format.createAudioSampleFormat( - "audio", - MimeTypes.AUDIO_AAC, - null, - Format.NO_VALUE, - Format.NO_VALUE, - 2, - 44100, - null, - null, - 0, - null); + private static final Format VIDEO_FORMAT = buildVideoFormat("video"); + private static final Format AUDIO_FORMAT = buildAudioFormat("audio"); private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup(VIDEO_FORMAT); private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup(AUDIO_FORMAT); private static final TrackGroupArray TRACK_GROUPS = @@ -339,12 +317,9 @@ public final class DefaultTrackSelectorTest { */ @Test public void testSelectTracksSelectTrackWithSelectionFlag() throws Exception { - Format audioFormat = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format audioFormat = buildAudioFormat("audio", /* language= */ null, /* selectionFlags= */ 0); Format formatWithSelectionFlag = - Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, null); + buildAudioFormat("audio", /* language= */ null, C.SELECTION_FLAG_DEFAULT); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -405,12 +380,8 @@ public final class DefaultTrackSelectorTest { */ @Test public void testSelectTracksPreferTrackWithinCapabilities() throws Exception { - Format supportedFormat = - Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); - Format exceededFormat = - Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format supportedFormat = buildAudioFormat("supportedFormat"); + Format exceededFormat = buildAudioFormat("exceededFormat"); Map mappedCapabilities = new HashMap<>(); mappedCapabilities.put(supportedFormat.id, FORMAT_HANDLED); @@ -781,10 +752,8 @@ public final class DefaultTrackSelectorTest { RendererCapabilities[] textRendererCapabilities = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; - TrackSelectorResult result; - // There is no text language preference, the first track flagged as default should be selected. - result = + TrackSelectorResult result = trackSelector.selectTracks( textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedDefault); @@ -878,10 +847,10 @@ public final class DefaultTrackSelectorTest { RendererCapabilities[] textRendererCapabilites = new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; - TrackSelectorResult result; - - result = trackSelector.selectTracks(textRendererCapabilites, - wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + TrackSelectorResult result = + trackSelector.selectTracks( + textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); trackSelector.setParameters( @@ -913,6 +882,48 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0)).isNull(); } + /** Tests audio track selection when there are multiple audio renderers. */ + @Test + public void testSelectPreferredTextTrackMultipleRenderers() throws Exception { + Format english = buildTextFormat("en", "en"); + Format german = buildTextFormat("de", "de"); + + // First renderer handles english. + Map firstRendererMappedCapabilities = new HashMap<>(); + firstRendererMappedCapabilities.put(english.id, FORMAT_HANDLED); + firstRendererMappedCapabilities.put(german.id, FORMAT_UNSUPPORTED_SUBTYPE); + RendererCapabilities firstRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_TEXT, firstRendererMappedCapabilities); + + // Second renderer handles german. + Map secondRendererMappedCapabilities = new HashMap<>(); + secondRendererMappedCapabilities.put(english.id, FORMAT_UNSUPPORTED_SUBTYPE); + secondRendererMappedCapabilities.put(german.id, FORMAT_HANDLED); + RendererCapabilities secondRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_TEXT, secondRendererMappedCapabilities); + + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] {firstRendererCapabilities, secondRendererCapabilities}; + + // Without an explicit language preference, nothing should be selected. + TrackSelectorResult result = + trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isNull(); + + // Explicit language preference for english. First renderer should be used. + trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("en")); + result = trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(english); + assertThat(result.selections.get(1)).isNull(); + + // Explicit language preference for German. Second renderer should be used. + trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("de")); + result = trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1).getFormat(0)).isSameAs(german); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -987,6 +998,50 @@ public final class DefaultTrackSelectorTest { .createTrackSelection(trackGroupArray.get(0), bandwidthMeter, 1, 2); } + /** Tests audio track selection when there are multiple audio renderers. */ + @Test + public void testSelectPreferredAudioTrackMultipleRenderers() throws Exception { + Format english = buildAudioFormat("en", "en"); + Format german = buildAudioFormat("de", "de"); + + // First renderer handles english. + Map firstRendererMappedCapabilities = new HashMap<>(); + firstRendererMappedCapabilities.put(english.id, FORMAT_HANDLED); + firstRendererMappedCapabilities.put(german.id, FORMAT_UNSUPPORTED_SUBTYPE); + RendererCapabilities firstRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, firstRendererMappedCapabilities); + + // Second renderer handles german. + Map secondRendererMappedCapabilities = new HashMap<>(); + secondRendererMappedCapabilities.put(english.id, FORMAT_UNSUPPORTED_SUBTYPE); + secondRendererMappedCapabilities.put(german.id, FORMAT_HANDLED); + RendererCapabilities secondRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, secondRendererMappedCapabilities); + + RendererCapabilities[] rendererCapabilities = + new RendererCapabilities[] {firstRendererCapabilities, secondRendererCapabilities}; + + // Without an explicit language preference, prefer the first renderer. + TrackSelectorResult result = + trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(english); + assertThat(result.selections.get(1)).isNull(); + + // Explicit language preference for english. First renderer should be used. + trackSelector.setParameters( + trackSelector.buildUponParameters().setPreferredAudioLanguage("en")); + result = trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(english); + assertThat(result.selections.get(1)).isNull(); + + // Explicit language preference for German. Second renderer should be used. + trackSelector.setParameters( + trackSelector.buildUponParameters().setPreferredAudioLanguage("de")); + result = trackSelector.selectTracks(rendererCapabilities, wrapFormats(english, german)); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1).getFormat(0)).isSameAs(german); + } + @Test public void testSelectTracksWithMultipleVideoTracksReturnsAdaptiveTrackSelection() throws Exception { @@ -1057,6 +1112,43 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(trackGroups); } + private static Format buildVideoFormat(String id) { + return Format.createVideoSampleFormat( + id, + MimeTypes.VIDEO_H264, + null, + Format.NO_VALUE, + Format.NO_VALUE, + 1024, + 768, + Format.NO_VALUE, + null, + null); + } + + private static Format buildAudioFormat(String id) { + return buildAudioFormat(id, /* language= */ null); + } + + private static Format buildAudioFormat(String id, String language) { + return buildAudioFormat(id, language, /* selectionFlags= */ 0); + } + + private static Format buildAudioFormat(String id, String language, int selectionFlags) { + return Format.createAudioSampleFormat( + id, + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* maxInputSize= */ Format.NO_VALUE, + /* channelCount= */ 2, + /* sampleRate= */ 44100, + /* initializationData= */ null, + /* drmInitData= */ null, + selectionFlags, + language); + } + private static Format buildTextFormat(String id, String language) { return buildTextFormat(id, language, /* selectionFlags= */ 0); }