diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index e25538a062..9d4049ada9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -519,6 +519,10 @@ public final class C { * A type constant for metadata tracks. */ public static final int TRACK_TYPE_METADATA = 4; + /** + * A type constant for a dummy or empty track. + */ + public static final int TRACK_TYPE_NONE = 5; /** * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or * equal to this value. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 5d55652f61..c0fc36964c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -21,11 +21,13 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.source.ClippingMediaPeriod; +import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -1343,24 +1345,22 @@ import java.io.IOException; readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i); - if (oldSelection == null) { - // The renderer has no current stream and will be enabled when we play the next period. + boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i]; + if (!rendererWasEnabled) { + // The renderer was disabled and will be enabled when we play the next period. } else if (initialDiscontinuity) { // The new period starts with a discontinuity, so the renderer will play out all data then // be disabled and re-enabled when it starts playing the next period. renderer.setCurrentStreamFinal(); } else if (!renderer.isCurrentStreamFinal()) { TrackSelection newSelection = newTrackSelectorResult.selections.get(i); + boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newSelection != null && newConfig.equals(oldConfig)) { + if (newRendererEnabled && newConfig.equals(oldConfig)) { // Replace the renderer's SampleStream so the transition to playing the next period can // be seamless. - Format[] formats = new Format[newSelection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } + Format[] formats = getFormats(newSelection); renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { @@ -1461,11 +1461,10 @@ import java.io.IOException; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { + if (periodHolder.trackSelectorResult.renderersEnabled[i]) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] && (newSelection == null + if (rendererWasEnabledFlags[i] && (!periodHolder.trackSelectorResult.renderersEnabled[i] || (renderer.isCurrentStreamFinal() && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not @@ -1487,49 +1486,63 @@ import java.io.IOException; enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } - private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) + private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount) throws ExoPlaybackException { - enabledRenderers = new Renderer[enabledRendererCount]; - enabledRendererCount = 0; + enabledRenderers = new Renderer[totalEnabledRendererCount]; + int enabledRendererCount = 0; for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { - enabledRenderers[enabledRendererCount++] = renderer; - if (renderer.getState() == Renderer.STATE_DISABLED) { - RendererConfiguration rendererConfiguration = - playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; - // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == Player.STATE_READY; - // 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()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } - // Enable the renderer. - renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i], - rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); - MediaClock mediaClock = renderer.getMediaClock(); - if (mediaClock != null) { - if (rendererMediaClock != null) { - throw ExoPlaybackException.createForUnexpected( - new IllegalStateException("Multiple renderer media clocks enabled.")); - } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; - rendererMediaClock.setPlaybackParameters(playbackParameters); - } - // Start the renderer if playing. - if (playing) { - renderer.start(); - } - } + if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { + enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); } } } + private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, + int enabledRendererIndex) throws ExoPlaybackException { + Renderer renderer = renderers[rendererIndex]; + enabledRenderers[enabledRendererIndex] = renderer; + if (renderer.getState() == Renderer.STATE_DISABLED) { + RendererConfiguration rendererConfiguration = + playingPeriodHolder.trackSelectorResult.rendererConfigurations[rendererIndex]; + TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get( + rendererIndex); + Format[] formats = getFormats(newSelection); + // The renderer needs enabling with its new track selection. + boolean playing = playWhenReady && state == Player.STATE_READY; + // Consider as joining only if the renderer was previously disabled. + boolean joining = !wasRendererEnabled && playing; + // Enable the renderer. + renderer.enable(rendererConfiguration, formats, + playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs, + joining, playingPeriodHolder.getRendererOffset()); + MediaClock mediaClock = renderer.getMediaClock(); + if (mediaClock != null) { + if (rendererMediaClock != null) { + throw ExoPlaybackException.createForUnexpected( + new IllegalStateException("Multiple renderer media clocks enabled.")); + } + rendererMediaClock = mediaClock; + rendererMediaClockSource = renderer; + rendererMediaClock.setPlaybackParameters(playbackParameters); + } + // Start the renderer if playing. + if (playing) { + renderer.start(); + } + } + } + + @NonNull + private static Format[] getFormats(TrackSelection newSelection) { + // Build an array of formats contained by the selection. + int length = newSelection != null ? newSelection.length() : 0; + Format[] formats = new Format[length]; + for (int i = 0; i < length; i++) { + formats[i] = newSelection.getFormat(i); + } + return formats; + } + /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ @@ -1656,17 +1669,24 @@ import java.io.IOException; && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i); } + // Undo the effect of previous call to associate no-sample renderers with empty tracks + // so the mediaPeriod receives back whatever it sent us before. + disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); + associateNoSampleRenderersWithEmptySampleStream(sampleStreams); periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; for (int i = 0; i < sampleStreams.length; i++) { if (sampleStreams[i] != null) { - Assertions.checkState(trackSelections.get(i) != null); - hasEnabledTracks = true; + Assertions.checkState(trackSelectorResult.renderersEnabled[i]); + // hasEnabledTracks should be true only when non-empty streams exists. + if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { + hasEnabledTracks = true; + } } else { Assertions.checkState(trackSelections.get(i) == null); } @@ -1690,6 +1710,31 @@ import java.io.IOException; } } + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy + * {@link EmptySampleStream} that was associated with it. + */ + private void disassociateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { + sampleStreams[i] = null; + } + } + } + + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will + * associate it with a dummy {@link EmptySampleStream}. + */ + private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + && trackSelectorResult.renderersEnabled[i]) { + sampleStreams[i] = new EmptySampleStream(); + } + } + } + } private static final class SeekPosition { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java new file mode 100644 index 0000000000..978f4f7a97 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -0,0 +1,278 @@ +/* + * 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; + +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaClock; +import java.io.IOException; + +/** + * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not + * consume data from its {@link SampleStream}. + */ +public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { + + private RendererConfiguration configuration; + private int index; + private int state; + private SampleStream stream; + private boolean streamIsFinal; + + @Override + public final int getTrackType() { + return C.TRACK_TYPE_NONE; + } + + @Override + public final RendererCapabilities getCapabilities() { + return this; + } + + @Override + public final void setIndex(int index) { + this.index = index; + } + + @Override + public MediaClock getMediaClock() { + return null; + } + + @Override + public final int getState() { + return state; + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_DISABLED}. + * + * @param configuration The renderer configuration. + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} from which the renderer should consume. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @param offsetUs The offset that should be subtracted from {@code positionUs} + * to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void enable(RendererConfiguration configuration, Format[] formats, + SampleStream stream, long positionUs, boolean joining, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(state == STATE_DISABLED); + this.configuration = configuration; + state = STATE_ENABLED; + onEnabled(joining); + replaceStream(formats, stream, offsetUs); + onPositionReset(positionUs, joining); + } + + @Override + public final void start() throws ExoPlaybackException { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_STARTED; + onStarted(); + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

+ * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. + * + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} to be associated with this renderer. + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(!streamIsFinal); + this.stream = stream; + onRendererOffsetChanged(offsetUs); + } + + @Override + public final SampleStream getStream() { + return stream; + } + + @Override + public final boolean hasReadStreamToEnd() { + return true; + } + + @Override + public final void setCurrentStreamFinal() { + streamIsFinal = true; + } + + @Override + public final boolean isCurrentStreamFinal() { + return streamIsFinal; + } + + @Override + public final void maybeThrowStreamError() throws IOException { + } + + @Override + public final void resetPosition(long positionUs) throws ExoPlaybackException { + streamIsFinal = false; + onPositionReset(positionUs, false); + } + + @Override + public final void stop() throws ExoPlaybackException { + Assertions.checkState(state == STATE_STARTED); + state = STATE_ENABLED; + onStopped(); + } + + @Override + public final void disable() { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_DISABLED; + stream = null; + streamIsFinal = false; + onDisabled(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public boolean isEnded() { + return true; + } + + // RendererCapabilities implementation. + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_NOT_SUPPORTED; + } + + // ExoPlayerComponent implementation. + + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + // Do nothing. + } + + // Methods to be overridden by subclasses. + + /** + * Called when the renderer is enabled. + *

+ * The default implementation is a no-op. + * + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onEnabled(boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer's offset has been changed. + *

+ * The default implementation is a no-op. + * + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onRendererOffsetChanged(long offsetUs) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the position is reset. This occurs when the renderer is enabled after + * {@link #onRendererOffsetChanged(long)} has been called, and also when a position + * discontinuity is encountered. + *

+ * The default implementation is a no-op. + * + * @param positionUs The new playback position in microseconds. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is started. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStarted() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is stopped. + *

+ * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStopped() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is disabled. + *

+ * The default implementation is a no-op. + */ + protected void onDisabled() { + // Do nothing. + } + + // Methods to be called by subclasses. + + /** + * Returns the configuration set when the renderer was most recently enabled. + */ + protected final RendererConfiguration getConfiguration() { + return configuration; + } + + /** + * Returns the index of the renderer within the player. + */ + protected final int getIndex() { + return index; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index d518b5a6be..5d120990fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -573,6 +573,8 @@ public abstract class MappingTrackSelector extends TrackSelector { } } + boolean[] rendererEnabled = determineEnabledRenderers(rendererCapabilities, trackSelections); + // Package up the track information and selections. MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes, rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports, @@ -583,14 +585,26 @@ public abstract class MappingTrackSelector extends TrackSelector { RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { - rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null; + rendererConfigurations[i] = rendererEnabled[i] ? RendererConfiguration.DEFAULT : null; } // Configure audio and video renderers to use tunneling if appropriate. maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays, rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId); - return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections), - mappedTrackInfo, rendererConfigurations); + return new TrackSelectorResult(trackGroups, rendererEnabled, + new TrackSelectionArray(trackSelections), mappedTrackInfo, rendererConfigurations); + } + + private boolean[] determineEnabledRenderers(RendererCapabilities[] rendererCapabilities, + TrackSelection[] trackSelections) { + boolean[] rendererEnabled = new boolean[trackSelections.length]; + for (int i = 0; i < rendererEnabled.length; i++) { + boolean forceRendererDisabled = rendererDisabledFlags.get(i); + rendererEnabled[i] = !forceRendererDisabled + && (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + || trackSelections[i] != null); + } + return rendererEnabled; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index cab9a689be..801f5b9584 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -28,6 +28,10 @@ public final class TrackSelectorResult { * The track groups that were provided to the {@link TrackSelector}. */ public final TrackGroupArray groups; + /** + * An array containing whether each renderer is enabled after the track selection operation. + */ + public final boolean[] renderersEnabled; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -38,21 +42,25 @@ public final class TrackSelectorResult { */ public final Object info; /** - * A {@link RendererConfiguration} for each renderer, to be used with the selections. + * A {@link RendererConfiguration} for each enabled renderer, to be used with the selections. */ public final RendererConfiguration[] rendererConfigurations; /** * @param groups The track groups provided to the {@link TrackSelector}. + * @param renderersEnabled An array containing whether each renderer is enabled after the track + * selection operation. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. * @param info An opaque object that will be returned to * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated. - * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used - * with the selections. + * @param rendererConfigurations A {@link RendererConfiguration} for each enabled renderer, + * to be used with the selections. */ - public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info, + public TrackSelectorResult(TrackGroupArray groups, boolean[] renderersEnabled, + TrackSelectionArray selections, Object info, RendererConfiguration[] rendererConfigurations) { this.groups = groups; + this.renderersEnabled = renderersEnabled; this.selections = selections; this.info = info; this.rendererConfigurations = rendererConfigurations; @@ -79,8 +87,8 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for the renderer at the given index. - * The results are equivalent if they have equal track selections and configurations for the - * renderer. + * The results are equivalent if they have equal renderersEnabled array, track selections, and + * configurations for the renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} * will be returned. @@ -92,7 +100,8 @@ public final class TrackSelectorResult { if (other == null) { return false; } - return Util.areEqual(selections.get(index), other.selections.get(index)) + return renderersEnabled[index] == other.renderersEnabled[index] + && Util.areEqual(selections.get(index), other.selections.get(index)) && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index cffc530354..b9ea0087c7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -15,15 +15,18 @@ */ package com.google.android.exoplayer2.trackselection; +import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -40,9 +43,15 @@ public final class MappingTrackSelectorTest { new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); private static final RendererCapabilities AUDIO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities NO_SAMPLE_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_NONE); private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { VIDEO_CAPABILITIES, AUDIO_CAPABILITIES }; + private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = + new RendererCapabilities[] { + VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, NO_SAMPLE_CAPABILITIES + }; private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup( Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE, @@ -58,6 +67,13 @@ public final class MappingTrackSelectorTest { new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) }; + private static final TrackSelection[] TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER = + new TrackSelection[] { + new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), + new FixedTrackSelection(AUDIO_TRACK_GROUP, 0), + null + }; + /** * Tests that the video and audio track groups are mapped onto the correct renderers. */ @@ -104,10 +120,14 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -115,11 +135,15 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isNull(); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {false, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -127,12 +151,16 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -140,12 +168,133 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], + * TrackGroupArray[], int[][][])} is propagated correctly to the result of + * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} + * when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that a null override clears a track selection when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithNullOverride() throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that a null override can be cleared when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithClearedNullOverride() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that an override is not applied for a different set of available track groups + * when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithNullOverrideForDifferentTracks() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that disabling another renderer works when there is no-sample renderer. + */ + @Test + public void testSelectTracksDisablingNormalRendererWithNoSampleRenderer() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setRendererDisabled(0, true); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that disabling no-sample renderer work. + */ + @Test + public void testSelectTracksDisablingNoSampleRenderer() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setRendererDisabled(2, true); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, false}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, null}) + .isEqualTo(result.rendererConfigurations); } /** @@ -166,7 +315,12 @@ public final class MappingTrackSelectorTest { TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) throws ExoPlaybackException { lastRendererTrackGroupArrays = rendererTrackGroupArrays; - return result == null ? new TrackSelection[rendererCapabilities.length] : result; + TrackSelection[] trackSelectionResult = new TrackSelection[rendererCapabilities.length]; + return result == null ? trackSelectionResult + // return a copy of the provided result, because MappingTrackSelector + // might modify the returned array here, and we don't want that to affect + // the original array. + : Arrays.asList(result).toArray(trackSelectionResult); } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) {