From 5bf4c249a28b13acb06f8cd27db5fbe497237cc8 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 11:10:54 -0800 Subject: [PATCH] Notify TrackSelection when it's enabled and disabled. Add onEnable() and onDisable() call-backs to TrackSelection. This allows TrackSelection to perform interesting operations (like subscribe to NetworkStatus) and clean up after itself. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175558485 --- .../android/exoplayer2/ExoPlayerTest.java | 141 ++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 33 +++- .../trackselection/BaseTrackSelection.java | 10 ++ .../trackselection/TrackSelection.java | 20 ++- .../testutil/FakeTrackSelection.java | 132 ++++++++++++++++ .../testutil/FakeTrackSelector.java | 86 +++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 56d5f05d00..0edd19bc09 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.FakeTrackSelection; +import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -311,4 +313,143 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made once (1 period). + // Track selections are not reused, so there are 2 track selections made. + assertEquals(2, createdTrackSelections.size()); + // There should be 2 track selections enabled in total. + assertEquals(2, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), + new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice (2 periods). + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // There should be 4 track selections enabled in total. + assertEquals(4, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // TrackSelections are reused, so there are only 2 track selections made for 2 renderers. + assertEquals(2, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + } 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 4d1767b64c..33889a2b57 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 @@ -1666,11 +1666,11 @@ import java.io.IOException; // 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); + updatePeriodTrackSelectorResult(trackSelectorResult); // 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; @@ -1692,6 +1692,7 @@ import java.io.IOException; } public void release() { + updatePeriodTrackSelectorResult(null); try { if (info.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); @@ -1704,6 +1705,36 @@ import java.io.IOException; } } + private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + if (periodTrackSelectorResult != null) { + disableTrackSelectionsInResult(periodTrackSelectorResult); + } + periodTrackSelectorResult = trackSelectorResult; + if (periodTrackSelectorResult != null) { + enableTrackSelectionsInResult(periodTrackSelectorResult); + } + } + + private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.enable(); + } + } + } + + private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.disable(); + } + } + } + /** * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy * {@link EmptySampleStream} that was associated with it. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 054ee7973f..6bc6afb88b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -78,6 +78,16 @@ public abstract class BaseTrackSelection implements TrackSelection { blacklistUntilTimes = new long[length]; } + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + @Override public final TrackGroup getTrackGroup() { return group; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index ad02b6c775..027b2abde9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -47,6 +47,20 @@ public interface TrackSelection { } + /** + * Enables the track selection. + *

+ * This method may not be called when the track selection is already enabled. + */ + void enable(); + + /** + * Disables this track selection. + *

+ * This method may only be called when the track selection is already enabled. + */ + void disable(); + /** * Returns the {@link TrackGroup} to which the selected tracks belong. */ @@ -124,6 +138,8 @@ public interface TrackSelection { /** * Updates the selected track. + *

+ * This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -150,7 +166,7 @@ public interface TrackSelection { * 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. + * track in this case. This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -167,6 +183,8 @@ public interface TrackSelection { * 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, long, long)}. + *

+ * This method may only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java new file mode 100644 index 0000000000..20346a0355 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 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.testutil; + +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.trackselection.TrackSelection; +import java.util.List; +import junit.framework.Assert; + +/** + * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number + * of calls to its methods. + */ +public final class FakeTrackSelection implements TrackSelection { + + private final TrackGroup rendererTrackGroup; + + public int enableCount; + public int releaseCount; + public boolean isEnabled; + + public FakeTrackSelection(TrackGroup rendererTrackGroup) { + this.rendererTrackGroup = rendererTrackGroup; + } + + @Override + public void enable() { + // assert that track selection is in disabled state before this call. + Assert.assertFalse(isEnabled); + enableCount++; + isEnabled = true; + } + + @Override + public void disable() { + // assert that track selection is in enabled state before this call. + Assert.assertTrue(isEnabled); + releaseCount++; + isEnabled = false; + } + + @Override + public TrackGroup getTrackGroup() { + return rendererTrackGroup; + } + + @Override + public int length() { + return rendererTrackGroup.length; + } + + @Override + public Format getFormat(int index) { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getIndexInTrackGroup(int index) { + return 0; + } + + @Override + public int indexOf(Format format) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public int indexOf(int indexInTrackGroup) { + return 0; + } + + @Override + public Format getSelectedFormat() { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getSelectedIndexInTrackGroup() { + return 0; + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Override + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { + Assert.assertTrue(isEnabled); + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public boolean blacklist(int index, long blacklistDurationMs) { + Assert.assertTrue(isEnabled); + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java new file mode 100644 index 0000000000..da9a1a18ad --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2017 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.testutil; + +import android.support.annotation.NonNull; +import com.google.android.exoplayer2.ExoPlaybackException; +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.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.ArrayList; +import java.util.List; + +/** + * A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. + */ +public class FakeTrackSelector extends MappingTrackSelector { + + private final List selectedTrackSelections = new ArrayList<>(); + private final boolean mayReuseTrackSelection; + + public FakeTrackSelector() { + this(false); + } + + /** + * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse + * {@link TrackSelection}s during track selection, when it finds previously-selected track + * selection using the same {@link TrackGroup}. + */ + public FakeTrackSelector(boolean mayReuseTrackSelection) { + this.mayReuseTrackSelection = mayReuseTrackSelection; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + List resultList = new ArrayList<>(); + for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { + TrackGroup trackGroup = trackGroupArray.get(0); + FakeTrackSelection trackSelectionForRenderer = reuseOrCreateTrackSelection(trackGroup); + resultList.add(trackSelectionForRenderer); + } + return resultList.toArray(new TrackSelection[resultList.size()]); + } + + @NonNull + private FakeTrackSelection reuseOrCreateTrackSelection(TrackGroup trackGroup) { + FakeTrackSelection trackSelectionForRenderer = null; + if (mayReuseTrackSelection) { + for (FakeTrackSelection selectedTrackSelection : selectedTrackSelections) { + if (selectedTrackSelection.getTrackGroup().equals(trackGroup)) { + trackSelectionForRenderer = selectedTrackSelection; + } + } + } + if (trackSelectionForRenderer == null) { + trackSelectionForRenderer = new FakeTrackSelection(trackGroup); + selectedTrackSelections.add(trackSelectionForRenderer); + } + return trackSelectionForRenderer; + } + + /** + * Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. + */ + public List getSelectedTrackSelections() { + return selectedTrackSelections; + } + +}