From b2a153d56815961f369988ba52364a0de4d577e3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 6 Jan 2017 04:17:07 -0800 Subject: [PATCH] Add a player test for renderers reading ahead. Also move timeline, manifest and format verifications into the test* methods. Issue: #2252 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=143761969 --- .../android/exoplayer2/ExoPlayerTest.java | 205 ++++++++++++------ 1 file changed, 142 insertions(+), 63 deletions(-) diff --git a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index c00f90f6623..0357fe8dad6 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.ArrayList; @@ -51,6 +52,8 @@ public final class ExoPlayerTest extends TestCase { private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); + private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null, + MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); /** * Tests playback of a source that exposes an empty timeline. Playback is expected to end without @@ -58,12 +61,17 @@ public final class ExoPlayerTest extends TestCase { */ public void testPlayEmptyTimeline() throws Exception { PlayerWrapper playerWrapper = new PlayerWrapper(); - playerWrapper.setup(Timeline.EMPTY, null, null); + Timeline timeline = Timeline.EMPTY; + MediaSource mediaSource = new FakeMediaSource(timeline, null); + FakeRenderer renderer = new FakeRenderer(null); + playerWrapper.setup(mediaSource, renderer); playerWrapper.blockUntilEnded(TIMEOUT_MS); assertEquals(0, playerWrapper.positionDiscontinuityCount); - assertEquals(0, playerWrapper.videoRenderer.formatReadCount); - assertEquals(0, playerWrapper.videoRenderer.bufferReadCount); - assertFalse(playerWrapper.videoRenderer.isEnded); + assertEquals(0, renderer.formatReadCount); + assertEquals(0, renderer.bufferReadCount); + assertFalse(renderer.isEnded); + assertEquals(timeline, playerWrapper.timeline); + assertNull(playerWrapper.manifest); } /** @@ -71,14 +79,19 @@ public void testPlayEmptyTimeline() throws Exception { */ public void testPlaySinglePeriodTimeline() throws Exception { PlayerWrapper playerWrapper = new PlayerWrapper(); - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 0)); - playerWrapper.setup(timeline, null, TEST_VIDEO_FORMAT); + Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Object manifest = new Object(); + MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); + playerWrapper.setup(mediaSource, renderer); playerWrapper.blockUntilEnded(TIMEOUT_MS); assertEquals(0, playerWrapper.positionDiscontinuityCount); - assertEquals(1, playerWrapper.videoRenderer.formatReadCount); - assertEquals(1, playerWrapper.videoRenderer.bufferReadCount); - assertTrue(playerWrapper.videoRenderer.isEnded); + assertEquals(1, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); + assertEquals(timeline, playerWrapper.timeline); + assertEquals(manifest, playerWrapper.manifest); + assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); } /** @@ -90,12 +103,58 @@ public void testPlayMultiPeriodTimeline() throws Exception { new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0)); - playerWrapper.setup(timeline, null, TEST_VIDEO_FORMAT); + MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); + FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); + playerWrapper.setup(mediaSource, renderer); playerWrapper.blockUntilEnded(TIMEOUT_MS); assertEquals(2, playerWrapper.positionDiscontinuityCount); - assertEquals(3, playerWrapper.videoRenderer.formatReadCount); - assertEquals(1, playerWrapper.videoRenderer.bufferReadCount); - assertTrue(playerWrapper.videoRenderer.isEnded); + assertEquals(3, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); + assertEquals(timeline, playerWrapper.timeline); + assertNull(playerWrapper.manifest); + } + + /** + * Tests that the player does not unnecessarily reset renderers when playing a multi-period + * source. + */ + public void testReadAheadToEndDoesNotResetRenderer() throws Exception { + final PlayerWrapper playerWrapper = new PlayerWrapper(); + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(false, false, 10), + new TimelineWindowDefinition(false, false, 10), + new TimelineWindowDefinition(false, false, 10)); + MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT, + TEST_AUDIO_FORMAT); + + FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT); + FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) { + + @Override + public long getPositionUs() { + // Simulate the playback position lagging behind the reading position: the renderer media + // clock position will be the start of the timeline until the stream is set to be final, at + // which point it jumps to the end of the timeline allowing the playing period to advance. + // TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US. + return isCurrentStreamFinal() ? 60000030 : 60000000; + } + + @Override + public boolean isEnded() { + // Allow playback to end once the final period is playing. + return playerWrapper.positionDiscontinuityCount == 2; + } + + }; + playerWrapper.setup(mediaSource, videoRenderer, audioRenderer); + playerWrapper.blockUntilEnded(TIMEOUT_MS); + assertEquals(2, playerWrapper.positionDiscontinuityCount); + assertEquals(1, audioRenderer.positionResetCount); + assertTrue(videoRenderer.isEnded); + assertTrue(audioRenderer.isEnded); + assertEquals(timeline, playerWrapper.timeline); + assertNull(playerWrapper.manifest); } /** @@ -107,14 +166,14 @@ private static final class PlayerWrapper implements ExoPlayer.EventListener { private final HandlerThread playerThread; private final Handler handler; - private Timeline expectedTimeline; - private Object expectedManifest; - private Format expectedFormat; - private FakeVideoRenderer videoRenderer; private ExoPlayer player; + private Timeline timeline; + private Object manifest; + private TrackGroupArray trackGroups; private Exception exception; - private int positionDiscontinuityCount; + // Written only on the main thread. + private volatile int positionDiscontinuityCount; public PlayerWrapper() { endedCountDownLatch = new CountDownLatch(1); @@ -136,20 +195,15 @@ public void blockUntilEnded(long timeoutMs) throws Exception { } } - public void setup(final Timeline timeline, final Object manifest, final Format format) { - expectedTimeline = timeline; - expectedManifest = manifest; - expectedFormat = format; + public void setup(final MediaSource mediaSource, final Renderer... renderers) { handler.post(new Runnable() { @Override public void run() { try { - videoRenderer = new FakeVideoRenderer(expectedFormat); - player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, - new DefaultTrackSelector()); + player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); player.addListener(PlayerWrapper.this); player.setPlayWhenReady(true); - player.prepare(new FakeMediaSource(timeline, manifest, format)); + player.prepare(mediaSource); } catch (Exception e) { handleError(e); } @@ -198,14 +252,13 @@ public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - assertEquals(expectedTimeline, timeline); - assertEquals(expectedManifest, manifest); + this.timeline = timeline; + this.manifest = manifest; } @Override - public void onTracksChanged(TrackGroupArray trackGroups, - TrackSelectionArray trackSelections) { - assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups); + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + this.trackGroups = trackGroups; } @Override @@ -213,6 +266,7 @@ public void onPlayerError(ExoPlaybackException exception) { handleError(exception); } + @SuppressWarnings("NonAtomicVolatileUpdate") @Override public void onPositionDiscontinuity() { positionDiscontinuityCount++; @@ -287,16 +341,20 @@ private static final class FakeMediaSource implements MediaSource { private final Timeline timeline; private final Object manifest; - private final Format format; + private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; private boolean preparedSource; private boolean releasedSource; - public FakeMediaSource(Timeline timeline, Object manifest, Format format) { + public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { this.timeline = timeline; this.manifest = manifest; - this.format = format; + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < formats.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + trackGroupArray = new TrackGroupArray(trackGroups); activeMediaPeriods = new ArrayList<>(); } @@ -318,7 +376,7 @@ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) assertTrue(preparedSource); assertFalse(releasedSource); assertEquals(0, positionUs); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(format); + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @@ -327,8 +385,9 @@ public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) public void releasePeriod(MediaPeriod mediaPeriod) { assertTrue(preparedSource); assertFalse(releasedSource); - assertTrue(activeMediaPeriods.remove(mediaPeriod)); - ((FakeMediaPeriod) mediaPeriod).release(); + FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod; + assertTrue(activeMediaPeriods.remove(fakeMediaPeriod)); + fakeMediaPeriod.release(); } @Override @@ -347,12 +406,12 @@ public void releaseSource() { */ private static final class FakeMediaPeriod implements MediaPeriod { - private final TrackGroup trackGroup; + private final TrackGroupArray trackGroupArray; private boolean preparedPeriod; - public FakeMediaPeriod(Format format) { - trackGroup = new TrackGroup(format); + public FakeMediaPeriod(TrackGroupArray trackGroupArray) { + this.trackGroupArray = trackGroupArray; } public void release() { @@ -374,26 +433,29 @@ public void maybeThrowPrepareError() throws IOException { @Override public TrackGroupArray getTrackGroups() { assertTrue(preparedPeriod); - return new TrackGroupArray(trackGroup); + return trackGroupArray; } @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { assertTrue(preparedPeriod); - assertEquals(1, selections.length); - assertEquals(1, mayRetainStreamFlags.length); - assertEquals(1, streams.length); - assertEquals(1, streamResetFlags.length); - assertEquals(0, positionUs); - if (streams[0] != null && (selections[0] == null || !mayRetainStreamFlags[0])) { - streams[0] = null; + int rendererCount = selections.length; + for (int i = 0; i < rendererCount; i++) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + streams[i] = null; + } } - if (streams[0] == null && selections[0] != null) { - FakeSampleStream stream = new FakeSampleStream(trackGroup.getFormat(0)); - assertEquals(trackGroup, selections[0].getTrackGroup()); - streams[0] = stream; - streamResetFlags[0] = true; + for (int i = 0; i < rendererCount; i++) { + if (streams[i] == null && selections[i] != null) { + TrackSelection selection = selections[i]; + assertEquals(1, selection.length()); + assertEquals(0, selection.getIndexInTrackGroup(0)); + TrackGroup trackGroup = selection.getTrackGroup(); + assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET); + streams[i] = new FakeSampleStream(trackGroup.getFormat(0)); + streamResetFlags[i] = true; + } } return 0; } @@ -474,24 +536,27 @@ public void skipToKeyframeBefore(long timeUs) { } /** - * Fake {@link Renderer} that supports any video format. The renderer verifies that it reads a - * given {@link Format}. + * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer + * verifies that it reads a given {@link Format}. */ - private static final class FakeVideoRenderer extends BaseRenderer { + private static class FakeRenderer extends BaseRenderer { private final Format expectedFormat; - private int formatReadCount; - private int bufferReadCount; - private boolean isEnded; + public int positionResetCount; + public int formatReadCount; + public int bufferReadCount; + public boolean isEnded; - public FakeVideoRenderer(Format expectedFormat) { - super(C.TRACK_TYPE_VIDEO); + public FakeRenderer(Format expectedFormat) { + super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN + : MimeTypes.getTrackType(expectedFormat.sampleMimeType)); this.expectedFormat = expectedFormat; } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + positionResetCount++; isEnded = false; } @@ -529,7 +594,21 @@ public boolean isEnded() { @Override public int supportsFormat(Format format) throws ExoPlaybackException { - return MimeTypes.isVideo(format.sampleMimeType) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED + : FORMAT_UNSUPPORTED_TYPE; + } + + } + + private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock { + + public FakeMediaClockRenderer(Format expectedFormat) { + super(expectedFormat); + } + + @Override + public MediaClock getMediaClock() { + return this; } }