diff --git a/demo/README.md b/demos/main/README.md similarity index 100% rename from demo/README.md rename to demos/main/README.md diff --git a/demo/build.gradle b/demos/main/build.gradle similarity index 98% rename from demo/build.gradle rename to demos/main/build.gradle index 7eea25478f..029a44326e 100644 --- a/demo/build.gradle +++ b/demos/main/build.gradle @@ -11,7 +11,7 @@ // 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. -apply from: '../constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.application' android { diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml similarity index 100% rename from demo/src/main/AndroidManifest.xml rename to demos/main/src/main/AndroidManifest.xml diff --git a/demo/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json similarity index 100% rename from demo/src/main/assets/media.exolist.json rename to demos/main/src/main/assets/media.exolist.json diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png similarity index 100% rename from demo/src/main/res/drawable-xhdpi/ic_banner.png rename to demos/main/src/main/res/drawable-xhdpi/ic_banner.png diff --git a/demo/src/main/res/layout/list_divider.xml b/demos/main/src/main/res/layout/list_divider.xml similarity index 100% rename from demo/src/main/res/layout/list_divider.xml rename to demos/main/src/main/res/layout/list_divider.xml diff --git a/demo/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml similarity index 100% rename from demo/src/main/res/layout/player_activity.xml rename to demos/main/src/main/res/layout/player_activity.xml diff --git a/demo/src/main/res/layout/sample_chooser_activity.xml b/demos/main/src/main/res/layout/sample_chooser_activity.xml similarity index 100% rename from demo/src/main/res/layout/sample_chooser_activity.xml rename to demos/main/src/main/res/layout/sample_chooser_activity.xml diff --git a/demo/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml similarity index 100% rename from demo/src/main/res/layout/track_selection_dialog.xml rename to demos/main/src/main/res/layout/track_selection_dialog.xml diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-hdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-mdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/demo/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml similarity index 100% rename from demo/src/main/res/values/strings.xml rename to demos/main/src/main/res/values/strings.xml diff --git a/demo/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml similarity index 100% rename from demo/src/main/res/values/styles.xml rename to demos/main/src/main/res/values/styles.xml diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 4282244a7a..203fd5e21c 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 06a356487e..c7050dbd0c 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 8c4fb4c51c..9bfe33e988 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -472,13 +472,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } if (!imaPlayingAd) { imaPlayingAd = true; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onPlay(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPlay(); } } else if (imaPausedInAd) { imaPausedInAd = false; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onResume(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onResume(); } } } @@ -509,8 +509,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return; } imaPausedInAd = true; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onPause(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPause(); } } @@ -555,8 +555,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); } } } @@ -569,8 +569,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onPlayerError(ExoPlaybackException error) { if (playingAd) { - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onError(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(); } } } @@ -630,8 +630,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); } } if (!wasPlayingAd && playingAd) { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java index 1d73234286..0162d22c34 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ForwardingTimeline; import com.google.android.exoplayer2.util.Assertions; /** * A {@link Timeline} for sources that have ads. */ -/* package */ final class SinglePeriodAdTimeline extends Timeline { +/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline { - private final Timeline contentTimeline; private final long[] adGroupTimesUs; private final int[] adCounts; private final int[] adsLoadedCounts; @@ -52,9 +52,9 @@ import com.google.android.exoplayer2.util.Assertions; public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) { + super(contentTimeline); Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1); - this.contentTimeline = contentTimeline; this.adGroupTimesUs = adGroupTimesUs; this.adCounts = adCounts; this.adsLoadedCounts = adsLoadedCounts; @@ -63,34 +63,13 @@ import com.google.android.exoplayer2.util.Assertions; this.adResumePositionUs = adResumePositionUs; } - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return 1; - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - contentTimeline.getPeriod(periodIndex, period, setIds); + timeline.getPeriod(periodIndex, period, setIds); period.set(period.id, period.uid, period.windowIndex, period.durationUs, period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts, adDurationsUs, adResumePositionUs); return period; } - @Override - public int getIndexOfPeriod(Object uid) { - return contentTimeline.getIndexOfPeriod(uid); - } - } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index abefe533ce..db0190de0f 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession; * See the License for the specific language governing permissions and * limitations under the License. */ - import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.PlaybackStateCompat; 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 bf4ea6e972..bc72ebc060 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 @@ -15,20 +15,18 @@ */ package com.google.android.exoplayer2; -import android.util.Pair; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.testutil.ExoPlayerWrapper; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.LinkedList; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import junit.framework.TestCase; /** @@ -43,67 +41,59 @@ public final class ExoPlayerTest extends TestCase { */ private static final int TIMEOUT_MS = 10000; - 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 * error. */ public void testPlayEmptyTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = Timeline.EMPTY; - MediaSource mediaSource = new FakeMediaSource(timeline, null); FakeRenderer renderer = new FakeRenderer(); - playerWrapper.setup(mediaSource, renderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(0, playerWrapper.positionDiscontinuityCount); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); 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); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setManifest(manifest).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertManifestsEqual(manifest); + testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest)); } /** * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0)); - 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); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertTimelinesEqual(timeline); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -111,16 +101,12 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); 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) { + final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @Override public long getPositionUs() { @@ -143,35 +129,30 @@ public final class ExoPlayerTest extends TestCase { @Override public boolean isEnded() { - // Allow playback to end once the final period is playing. - return playerWrapper.positionDiscontinuityCount == 2; + return videoRenderer.isEnded(); } }; - playerWrapper.setup(mediaSource, videoRenderer, audioRenderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(2, playerWrapper.positionDiscontinuityCount); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer) + .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertTimelinesEqual(timeline); assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); assertTrue(audioRenderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } public void testRepreparationGivesFreshSourceInfo() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); - FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); - - // Prepare the player with a source with the first manifest and a non-empty timeline + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); - playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT), - renderer); - playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS); - - // Prepare the player again with a source and a new manifest, which will never be exposed. + MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, + Builder.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); - playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) { + MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { super.prepareSource(player, isTopLevelSource, listener); @@ -185,29 +166,49 @@ public final class ExoPlayerTest extends TestCase { throw new IllegalStateException(e); } } - }); - - // Prepare the player again with a third source. - queuedSourceInfoCountDownLatch.await(); + }; Object thirdSourceManifest = new Object(); - playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT)); - completePreparationCountDownLatch.countDown(); - - // Wait for playback to complete. - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(0, playerWrapper.positionDiscontinuityCount); - assertEquals(1, renderer.formatReadCount); - assertEquals(1, renderer.bufferReadCount); - assertTrue(renderer.isEnded); - assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); + MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest, + Builder.VIDEO_FORMAT); + // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare + // the player again with a source and a new manifest, which will never be exposed. Allow the + // test thread to prepare the player with a third source, and block the playback thread until + // the test thread's call to prepare() has returned. + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation") + .waitForTimelineChanged(timeline) + .prepareSource(secondSource) + .executeRunnable(new Runnable() { + @Override + public void run() { + try { + queuedSourceInfoCountDownLatch.await(); + } catch (InterruptedException e) { + // Ignore. + } + } + }) + .prepareSource(thirdSource) + .executeRunnable(new Runnable() { + @Override + public void run() { + completePreparationCountDownLatch.countDown(); + } + }) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); // The first source's preparation completed with a non-empty timeline. When the player was // re-prepared with the second source, it immediately exposed an empty timeline, but the source // info refresh from the second source was suppressed as we re-prepared with the third source. - playerWrapper.assertSourceInfosEquals( - Pair.create(timeline, firstSourceManifest), - Pair.create(Timeline.EMPTY, null), - Pair.create(timeline, thirdSourceManifest)); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); + testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); + assertEquals(1, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); } public void testRepeatModeChanges() throws Exception { @@ -215,49 +216,22 @@ public final class ExoPlayerTest extends TestCase { new TimelineWindowDefinition(true, false, 100000), new TimelineWindowDefinition(true, false, 100000), new TimelineWindowDefinition(true, false, 100000)); - final int[] actionSchedule = { // 0 -> 1 - Player.REPEAT_MODE_ONE, // 1 -> 1 - Player.REPEAT_MODE_OFF, // 1 -> 2 - Player.REPEAT_MODE_ONE, // 2 -> 2 - Player.REPEAT_MODE_ALL, // 2 -> 0 - Player.REPEAT_MODE_ONE, // 0 -> 0 - -1, // 0 -> 0 - Player.REPEAT_MODE_OFF, // 0 -> 1 - -1, // 1 -> 2 - -1 // 2 -> ended - }; - int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2}; - final LinkedList windowIndices = new LinkedList<>(); - final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() { - @Override - @SuppressWarnings("ResourceType") - public void onPositionDiscontinuity() { - super.onPositionDiscontinuity(); - int actionIndex = actionSchedule.length - (int) actionCounter.getCount(); - if (actionSchedule[actionIndex] != -1) { - player.setRepeatMode(actionSchedule[actionIndex]); - } - windowIndices.add(player.getCurrentWindowIndex()); - actionCounter.countDown(); - } - }; - MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); - FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); - playerWrapper.setup(mediaSource, renderer); - boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - playerWrapper.release(); - assertTrue("Test playback timed out waiting for action schedule to end.", finished); - if (playerWrapper.exception != null) { - throw playerWrapper.exception; - } - assertEquals(expectedWindowIndices.length, windowIndices.size()); - for (int i = 0; i < expectedWindowIndices.length; i++) { - assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue()); - } - assertEquals(9, playerWrapper.positionDiscontinuityCount); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 2 -> 2 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ALL) // 2 -> 0 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 0 -> 0 + .waitForPositionDiscontinuity() // 0 -> 0 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 0 -> end + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); + testRunner.assertTimelinesEqual(timeline); assertTrue(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 9f5b067b5e..43c867f435 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.drm; import static org.mockito.Matchers.any; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index d52deb108f..1acc208c29 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(4000, random), - new byte[]{'O', 'g', 'g', 'S'}, + new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); @@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(2046, random), - new byte[]{'O', 'g', 'g', 'S'}, + new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); @@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( - new byte[]{'x', 'O', 'g', 'g', 'S'} + new byte[] {'x', 'O', 'g', 'g', 'S'} ), false); skipToNextPage(extractorInput); assertEquals(1, extractorInput.getPosition()); @@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { public void testSkipToNextPageNoMatch() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( - new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false); + new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); try { skipToNextPage(extractorInput); fail(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java index bcfa90a565..6a31250e15 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/AdtsReaderTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase { } } - public void testAdtsDataOnly() throws Exception { + public void testAdtsDataOnly() throws ParserException { data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length); feed(); assertSampleCounts(0, 1); adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null); } - private void feedLimited(int limit) { + private void feedLimited(int limit) throws ParserException { maybeStartPacket(); data.setLimit(limit); feed(); } - private void feed() { + private void feed() throws ParserException { maybeStartPacket(); adtsReader.consume(data); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 76ea0e34cf..77e61e39a9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase { public void testAdvanceToBeforeBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); + int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); // Should fail and have no effect. - assertFalse(result); + assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToStartOfBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); + int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); // Should succeed but have no effect (we're already at the first frame). - assertTrue(result); + assertEquals(0, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToEndOfBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); - // Should succeed and skip to 2nd keyframe. - assertTrue(result); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); + // Should succeed and skip to 2nd keyframe (the 4th frame). + assertEquals(4, skipCount); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToAfterBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); // Should fail and have no effect. - assertFalse(result); + assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToAfterBufferAllowed() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); - // Should succeed and skip to 2nd keyframe. - assertTrue(result); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); + // Should succeed and skip to 2nd keyframe (the 4th frame). + assertEquals(4, skipCount); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java new file mode 100644 index 0000000000..5de6bdf3e1 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -0,0 +1,134 @@ +/* + * 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.source; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import junit.framework.TestCase; + +/** + * Unit test for {@link ShuffleOrder}. + */ +public final class ShuffleOrderTest extends TestCase { + + public static final long RANDOM_SEED = 1234567890L; + + public void testDefaultShuffleOrder() { + assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5); + } + } + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4); + testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); + } + + public void testUnshuffledShuffleOrder() { + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5); + } + } + testCloneAndRemove(new UnshuffledShuffleOrder(5), 0); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 2); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 4); + testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); + } + + public void testUnshuffledShuffleOrderIsUnshuffled() { + ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); + assertEquals(0, shuffleOrder.getFirstIndex()); + assertEquals(4, shuffleOrder.getLastIndex()); + for (int i = 0; i < 4; i++) { + assertEquals(i + 1, shuffleOrder.getNextIndex(i)); + } + } + + private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) { + assertEquals(length, shuffleOrder.getLength()); + if (length == 0) { + assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex()); + } else { + int[] indices = new int[length]; + indices[0] = shuffleOrder.getFirstIndex(); + assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0])); + for (int i = 1; i < length; i++) { + indices[i] = shuffleOrder.getNextIndex(indices[i - 1]); + assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i])); + for (int j = 0; j < i; j++) { + assertTrue(indices[i] != indices[j]); + } + } + assertEquals(indices[length - 1], shuffleOrder.getLastIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1])); + for (int i = 0; i < length; i++) { + assertTrue(indices[i] >= 0 && indices[i] < length); + } + } + } + + private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) { + ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex += count; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count); + while (newNextIndex >= position && newNextIndex < position + count) { + newNextIndex = newOrder.getNextIndex(newNextIndex); + } + assertEquals(expectedNextIndex, newNextIndex); + } + } + + private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) { + ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + if (i == position) { + continue; + } + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex == position) { + expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex); + } + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex--; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); + assertEquals(expectedNextIndex, newNextIndex); + } + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index 6c5d7c76f7..b4f1d50293 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.util; import android.test.InstrumentationTestCase; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index cfb9cd78be..d7b2b36740 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.util; import android.test.MoreAsserts; - import junit.framework.TestCase; /** @@ -27,8 +26,14 @@ public final class ParsableBitArrayTest extends TestCase { private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; + private ParsableBitArray testArray; + + @Override + public void setUp() { + testArray = new ParsableBitArray(TEST_DATA); + } + public void testReadAllBytes() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); byte[] bytesRead = new byte[TEST_DATA.length]; testArray.readBytes(bytesRead, 0, TEST_DATA.length); MoreAsserts.assertEquals(TEST_DATA, bytesRead); @@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase { } public void testReadBit() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); - assertReadBitsToEnd(0, testArray); + assertReadBitsToEnd(0); } public void testReadBits() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); + assertEquals(getTestDataBits(5, 0), testArray.readBits(0)); assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); @@ -52,67 +56,101 @@ public final class ParsableBitArrayTest extends TestCase { assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); } + public void testReadBitsToByteArray() { + byte[] result = new byte[TEST_DATA.length]; + // Test read within byte boundaries. + testArray.readBits(result, 0, 6); + assertEquals(TEST_DATA[0] & 0xFC, result[0]); + // Test read across byte boundaries. + testArray.readBits(result, 0, 8); + assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]); + // Test reading across multiple bytes. + testArray.readBits(result, 1, 50); + for (int i = 1; i < 7; i++) { + assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)), + result[i]); + } + assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]); + assertEquals(0, testArray.bitsLeft()); + // Test read last buffer byte across input data bytes. + testArray.setPosition(31); + result[3] = 0; + testArray.readBits(result, 3, 3); + assertEquals((byte) 0xE0, result[3]); + // Test read bits in the middle of a input data byte. + result[0] = 0; + assertEquals(34, testArray.getPosition()); + testArray.readBits(result, 0, 3); + assertEquals((byte) 0xE0, result[0]); + // Test read 0 bits. + testArray.setPosition(32); + result[1] = 0; + testArray.readBits(result, 1, 0); + assertEquals(0, result[1]); + // Test reading a number of bits divisible by 8. + testArray.setPosition(0); + testArray.readBits(result, 0, 16); + assertEquals(TEST_DATA[0], result[0]); + assertEquals(TEST_DATA[1], result[1]); + // Test least significant bits are unmodified. + result[1] = (byte) 0xFF; + testArray.readBits(result, 0, 9); + assertEquals(0x5F, result[0]); + assertEquals(0x7F, result[1]); + } + public void testRead32BitsByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); } public void testRead32BitsNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); } public void testSkipBytes() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBytes(2); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSkipBitsByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBits(16); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSkipBitsNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBits(5); - assertReadBitsToEnd(5, testArray); + assertReadBitsToEnd(5); } public void testSetPositionByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(16); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSetPositionNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(5); - assertReadBitsToEnd(5, testArray); + assertReadBitsToEnd(5); } public void testByteAlignFromNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(11); testArray.byteAlign(); assertEquals(2, testArray.getBytePosition()); assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testByteAlignFromByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(16); testArray.byteAlign(); // Should be a no-op. assertEquals(2, testArray.getBytePosition()); assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } - private static void assertReadBitsToEnd(int expectedStartPosition, ParsableBitArray testArray) { + private void assertReadBitsToEnd(int expectedStartPosition) { int position = testArray.getPosition(); assertEquals(expectedStartPosition, position); for (int i = position; i < TEST_DATA.length * 8; i++) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 49719b95f7..324d668c7a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianLong() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF }); @@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, (byte) 0xFF }); assertEquals(0xFF000001, byteArray.readLittleEndianInt()); @@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianUnsignedShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort()); @@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); assertEquals((short) 0xFF01, byteArray.readLittleEndianShort()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a88a1dd615..7f14837965 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * {@code positionUs} is beyond it. * * @param positionUs The position in microseconds. + * @return The number of samples that were skipped. */ - protected void skipSource(long positionUs) { - stream.skipData(positionUs - streamOffsetUs); + protected int skipSource(long positionUs) { + return stream.skipData(positionUs - streamOffsetUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 08e178878b..db1b1a13f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. - player = new ExoPlayerImpl(renderers, trackSelector, loadControl); + player = createExoPlayerImpl(renderers, trackSelector, loadControl); } /** @@ -723,6 +723,19 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. + /** + * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * + * @param renderers The {@link Renderer}s that will be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @return A new {@link ExoPlayer} instance. + */ + protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + return new ExoPlayerImpl(renderers, trackSelector, loadControl); + } + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index 3c2d6d96e9..7a532110d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -36,6 +36,12 @@ public final class DecoderCounters { * The number of queued input buffers. */ public int inputBufferCount; + /** + * The number of skipped input buffers. + *

+ * A skipped input buffer is an input buffer that was deliberately not sent to the decoder. + */ + public int skippedInputBufferCount; /** * The number of rendered output buffers. */ @@ -79,6 +85,7 @@ public final class DecoderCounters { decoderInitCount += other.decoderInitCount; decoderReleaseCount += other.decoderReleaseCount; inputBufferCount += other.inputBufferCount; + skippedInputBufferCount += other.skippedInputBufferCount; renderedOutputBufferCount += other.renderedOutputBufferCount; skippedOutputBufferCount += other.skippedOutputBufferCount; droppedOutputBufferCount += other.droppedOutputBufferCount; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 040ca50c76..2eb3463b3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.drm; import android.media.MediaDrm; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java index 2f21898007..ec5ad88aeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/flv/AudioTagPayloadReader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -85,7 +86,7 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) { + protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index f7e3e846e9..0a5e0e8a6d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -816,7 +816,7 @@ import java.util.List; private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, - StsdData out, int entryIndex) { + StsdData out, int entryIndex) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 7277df5bb8..96b964a4c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -19,6 +19,7 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader { } @Override - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader { /** * Parses the sample header. */ - private void parseAdtsHeader() { + private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); if (!hasOutputFormat) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index 40cfd7f8d9..bd013f96a3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA_LSF: return new PesReader(new MpegAudioReader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_AAC: + case TsExtractor.TS_STREAM_TYPE_AAC_ADTS: return isSet(FLAG_IGNORE_AAC_STREAM) ? null : new PesReader(new AdtsReader(false, esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AAC_LATM: + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new LatmReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java index 57bcf31fc5..fa7f78c8c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/ElementaryStreamReader.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -50,8 +51,9 @@ public interface ElementaryStreamReader { * Consumes (possibly partial) data from the current packet. * * @param data The data to consume. + * @throws ParserException If the data could not be parsed. */ - void consume(ParsableByteArray data); + void consume(ParsableByteArray data) throws ParserException; /** * Called when a packet ends. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java new file mode 100644 index 0000000000..d06c6f0cb4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -0,0 +1,307 @@ +/* + * 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.extractor.ts; + +import android.support.annotation.Nullable; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ExtractorOutput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; + +/** + * Parses and extracts samples from an AAC/LATM elementary stream. + */ +public final class LatmReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC_1 = 0; + private static final int STATE_FINDING_SYNC_2 = 1; + private static final int STATE_READING_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; + + private static final int INITIAL_BUFFER_SIZE = 1024; + private static final int SYNC_BYTE_FIRST = 0x56; + private static final int SYNC_BYTE_SECOND = 0xE0; + + private final String language; + private final ParsableByteArray sampleDataBuffer; + private final ParsableBitArray sampleBitArray; + + // Track output info. + private TrackOutput output; + private Format format; + private String formatId; + + // Parser state info. + private int state; + private int bytesRead; + private int sampleSize; + private int secondHeaderByte; + private long timeUs; + + // Container data. + private boolean streamMuxRead; + private int audioMuxVersion; + private int audioMuxVersionA; + private int numSubframes; + private int frameLengthType; + private boolean otherDataPresent; + private long otherDataLenBits; + private int sampleRateHz; + private long sampleDurationUs; + private int channelCount; + + /** + * @param language Track language. + */ + public LatmReader(@Nullable String language) { + this.language = language; + sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE); + sampleBitArray = new ParsableBitArray(sampleDataBuffer.data); + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC_1; + streamMuxRead = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + formatId = idGenerator.getFormatId(); + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) throws ParserException { + int bytesToRead; + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC_1: + if (data.readUnsignedByte() == SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_2; + } + break; + case STATE_FINDING_SYNC_2: + int secondByte = data.readUnsignedByte(); + if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) { + secondHeaderByte = secondByte; + state = STATE_READING_HEADER; + } else if (secondByte != SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_1; + } + break; + case STATE_READING_HEADER: + sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte(); + if (sampleSize > sampleDataBuffer.data.length) { + resetBufferForSize(sampleSize); + } + bytesRead = 0; + state = STATE_READING_SAMPLE; + break; + case STATE_READING_SAMPLE: + bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + data.readBytes(sampleBitArray.data, bytesRead, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + sampleBitArray.setPosition(0); + parseAudioMuxElement(sampleBitArray); + state = STATE_FINDING_SYNC_1; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41. + * + * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. + */ + private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { + boolean useSameStreamMux = data.readBit(); + if (!useSameStreamMux) { + streamMuxRead = true; + parseStreamMuxConfig(data); + } else if (!streamMuxRead) { + return; // Parsing cannot continue without StreamMuxConfig information. + } + + if (audioMuxVersionA == 0) { + if (numSubframes != 0) { + throw new ParserException(); + } + int muxSlotLengthBytes = parsePayloadLengthInfo(data); + parsePayloadMux(data, muxSlotLengthBytes); + if (otherDataPresent) { + data.skipBits((int) otherDataLenBits); + } + } else { + throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009. + } + } + + /** + * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. + */ + private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { + audioMuxVersion = data.readBits(1); + audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; + if (audioMuxVersionA == 0) { + if (audioMuxVersion == 1) { + latmGetValue(data); // Skip taraBufferFullness. + } + if (!data.readBit()) { + throw new ParserException(); + } + numSubframes = data.readBits(6); + int numProgram = data.readBits(4); + int numLayer = data.readBits(3); + if (numProgram != 0 || numLayer != 0) { + throw new ParserException(); + } + if (audioMuxVersion == 0) { + int startPosition = data.getPosition(); + int readBits = parseAudioSpecificConfig(data); + data.setPosition(startPosition); + byte[] initData = new byte[(readBits + 7) / 8]; + data.readBits(initData, 0, readBits); + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, + Collections.singletonList(initData), null, 0, language); + if (!format.equals(this.format)) { + this.format = format; + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; + output.format(format); + } + } else { + int ascLen = (int) latmGetValue(data); + int bitsRead = parseAudioSpecificConfig(data); + data.skipBits(ascLen - bitsRead); // fillBits. + } + parseFrameLength(data); + otherDataPresent = data.readBit(); + otherDataLenBits = 0; + if (otherDataPresent) { + if (audioMuxVersion == 1) { + otherDataLenBits = latmGetValue(data); + } else { + boolean otherDataLenEsc; + do { + otherDataLenEsc = data.readBit(); + otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8); + } while (otherDataLenEsc); + } + } + boolean crcCheckPresent = data.readBit(); + if (crcCheckPresent) { + data.skipBits(8); // crcCheckSum. + } + } else { + throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009. + } + } + + private void parseFrameLength(ParsableBitArray data) { + frameLengthType = data.readBits(3); + switch (frameLengthType) { + case 0: + data.skipBits(8); // latmBufferFullness. + break; + case 1: + data.skipBits(9); // frameLength. + break; + case 3: + case 4: + case 5: + data.skipBits(6); // CELPframeLengthTableIndex. + break; + case 6: + case 7: + data.skipBits(1); // HVXCframeLengthTableIndex. + break; + } + } + + private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { + int bitsLeft = data.bitsLeft(); + Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true); + sampleRateHz = config.first; + channelCount = config.second; + return bitsLeft - data.bitsLeft(); + } + + private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException { + int muxSlotLengthBytes = 0; + // Assuming single program and single layer. + if (frameLengthType == 0) { + int tmp; + do { + tmp = data.readBits(8); + muxSlotLengthBytes += tmp; + } while (tmp == 255); + return muxSlotLengthBytes; + } else { + throw new ParserException(); + } + } + + private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { + // The start of sample data in + int bitPosition = data.getPosition(); + if ((bitPosition & 0x07) == 0) { + // Sample data is byte-aligned. We can output it directly. + sampleDataBuffer.setPosition(bitPosition >> 3); + } else { + // Sample data is not byte-aligned and we need align it ourselves before outputting. + // Byte alignment is needed because LATM framing is not supported by MediaCodec. + data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8); + sampleDataBuffer.setPosition(0); + } + output.sampleData(sampleDataBuffer, muxLengthBytes); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null); + timeUs += sampleDurationUs; + } + + private void resetBufferForSize(int newSize) { + sampleDataBuffer.reset(newSize); + sampleBitArray.reset(sampleDataBuffer.data); + } + + private static long latmGetValue(ParsableBitArray data) { + int bytesForValue = data.readBits(2); + return data.readBits((bytesForValue + 1) * 8); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 59696b9dea..4863df42eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader { } @Override - public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) + throws ParserException { if (payloadUnitStartIndicator) { switch (state) { case STATE_FINDING_HEADER: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java index 883fb8f880..69c5745eaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/PsExtractor.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Facilitates the extraction of data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { @@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor { * Consumes the payload of a PS packet. * * @param data The PES packet. The position will be set to the start of the payload. + * @throws ParserException If the payload could not be parsed. */ - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { data.readBytes(pesScratch.data, 0, 3); pesScratch.setPosition(0); parseHeader(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2929b8a076..90506ab2f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -84,7 +84,8 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; - public static final int TS_STREAM_TYPE_AAC = 0x0F; + public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; + public static final int TS_STREAM_TYPE_AAC_LATM = 0x11; public static final int TS_STREAM_TYPE_AC3 = 0x81; public static final int TS_STREAM_TYPE_DTS = 0x8A; public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index e7996c66c3..efa764b572 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -196,7 +197,8 @@ public interface TsPayloadReader { * * @param data The TS packet. The position will be set to the start of the payload. * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @throws ParserException If the payload could not be parsed. */ - void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); + void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 01229c1104..7c0549de25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -530,7 +530,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { while (feedInputBuffer()) {} TraceUtil.endSection(); } else { - skipSource(positionUs); + decoderCounters.skippedInputBufferCount += skipSource(positionUs); // We need to read any format changes despite not having a codec so that drmSession can be // updated, and so that we have the most recent format should the codec be initialized. We may // also reach the end of the stream. Note that readSource will not read a sample into a diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 12f58d9a21..a8c33b4625 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -286,8 +286,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void skipData(long positionUs) { - stream.skipData(startUs + positionUs); + public int skipData(long positionUs) { + return stream.skipData(startUs + positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 32c4eb6c73..4caafa3110 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -128,9 +127,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste /** * Provides a clipped view of a specified timeline. */ - private static final class ClippingTimeline extends Timeline { + private static final class ClippingTimeline extends ForwardingTimeline { - private final Timeline timeline; private final long startUs; private final long endUs; @@ -143,6 +141,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. */ public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + super(timeline); Assertions.checkArgument(timeline.getWindowCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1); Window window = timeline.getWindow(0, new Window(), false); @@ -155,26 +154,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste } Period period = timeline.getPeriod(0, new Period()); Assertions.checkArgument(period.getPositionInWindowUs() == 0); - this.timeline = timeline; this.startUs = startUs; this.endUs = resolvedEndUs; } - @Override - public int getWindowCount() { - return 1; - } - - @Override - public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) { - return timeline.getNextWindowIndex(windowIndex, repeatMode); - } - - @Override - public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) { - return timeline.getPreviousWindowIndex(windowIndex, repeatMode); - } - @Override public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { @@ -196,11 +179,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return window; } - @Override - public int getPeriodCount() { - return 1; - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { period = timeline.getPeriod(0, period, setIds); @@ -208,11 +186,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return period; } - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(uid); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index b00732e839..c2d2e5f11e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError(); + for (int i = 0; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError(); } } @@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override public void releaseSource() { - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - mediaSourceHolder.mediaSource.releaseSource(); + for (int i = 0; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).mediaSource.releaseSource(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java index 7aab22d8a0..299b816cc8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e7273f834b..511f7f4a8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -238,7 +238,7 @@ import java.util.Arrays; // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED && sampleQueue.getReadIndex() != 0; } } @@ -371,12 +371,13 @@ import java.util.Arrays; lastSeekPositionUs); } - /* package */ void skipData(int track, long positionUs) { + /* package */ int skipData(int track, long positionUs) { SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } @@ -558,7 +559,8 @@ import java.util.Arrays; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) + != SampleQueue.ADVANCE_FAILED; // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -632,8 +634,8 @@ import java.util.Arrays; } @Override - public void skipData(long positionUs) { - ExtractorMediaPeriod.this.skipData(track, positionUs); + public int skipData(long positionUs) { + return ExtractorMediaPeriod.this.skipData(track, positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java new file mode 100644 index 0000000000..4203abbf39 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -0,0 +1,68 @@ +/* + * 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.source; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; + +/** + * An overridable {@link Timeline} implementation forwarding all methods to another timeline. + */ +public abstract class ForwardingTimeline extends Timeline { + + protected final Timeline timeline; + + public ForwardingTimeline(Timeline timeline) { + this.timeline = timeline; + } + + @Override + public int getWindowCount() { + return timeline.getWindowCount(); + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + return timeline.getNextWindowIndex(windowIndex, repeatMode); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return timeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return timeline.getPeriod(periodIndex, period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(uid); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index a6e93a92b9..1795fe8045 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -160,53 +160,25 @@ public final class LoopingMediaSource implements MediaSource { } - private static final class InfinitelyLoopingTimeline extends Timeline { + private static final class InfinitelyLoopingTimeline extends ForwardingTimeline { - private final Timeline childTimeline; - - public InfinitelyLoopingTimeline(Timeline childTimeline) { - this.childTimeline = childTimeline; - } - - @Override - public int getWindowCount() { - return childTimeline.getWindowCount(); + public InfinitelyLoopingTimeline(Timeline timeline) { + super(timeline); } @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode); + int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode); return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode); + int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode); return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 : childPreviousWindowIndex; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return childTimeline.getPeriodCount(); - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return childTimeline.getPeriod(periodIndex, period, setIds); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return childTimeline.getIndexOfPeriod(uid); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 03b2e3b715..d70c59b195 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -253,32 +253,35 @@ import com.google.android.exoplayer2.util.Util; * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * end of the queue, by advancing the read position to the last sample (or keyframe) in the * queue. - * @return Whether the operation was a success. A successful advance is one in which the read - * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + * @return The number of samples that were skipped if the operation was successful, which may be + * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A + * successful advance is one in which the read position was unchanged or advanced, and is now + * at a sample meeting the specified criteria. */ - public synchronized boolean advanceTo(long timeUs, boolean toKeyframe, + public synchronized int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { int relativeReadIndex = getRelativeIndex(readPosition); if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { - return false; + return SampleQueue.ADVANCE_FAILED; } int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); if (offset == -1) { - return false; + return SampleQueue.ADVANCE_FAILED; } readPosition += offset; - return true; + return offset; } /** * Advances the read position to the end of the queue. + * + * @return The number of samples that were skipped. */ - public synchronized void advanceToEnd() { - if (!hasNextSample()) { - return; - } + public synchronized int advanceToEnd() { + int skipCount = length - readPosition; readPosition = length; + return skipCount; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index c7bae8f8b4..b83cf7df5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -49,6 +49,8 @@ public final class SampleQueue implements TrackOutput { } + public static final int ADVANCE_FAILED = -1; + private static final int INITIAL_SCRATCH_SIZE = 32; private final Allocator allocator; @@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput { /** * Advances the read position to the end of the queue. + * + * @return The number of samples that were skipped. */ - public void advanceToEnd() { - metadataQueue.advanceToEnd(); + public int advanceToEnd() { + return metadataQueue.advanceToEnd(); } /** @@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput { * time, rather than to any sample before or at that time. * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * end of the queue, by advancing the read position to the last sample (or keyframe). - * @return Whether the operation was a success. A successful advance is one in which the read - * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + * @return The number of samples that were skipped if the operation was successful, which may be + * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful + * advance is one in which the read position was unchanged or advanced, and is now at a sample + * meeting the specified criteria. */ - public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { + public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index dc58c29c22..06efc980e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -70,7 +70,8 @@ public interface SampleStream { * {@code positionUs} is beyond it. * * @param positionUs The specified time. + * @return The number of samples that were skipped. */ - void skipData(long positionUs); + int skipData(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java new file mode 100644 index 0000000000..4307fd2c19 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java @@ -0,0 +1,256 @@ +/* + * 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.source; + +import com.google.android.exoplayer2.C; +import java.util.Arrays; +import java.util.Random; + +/** + * Shuffled order of indices. + */ +public interface ShuffleOrder { + + /** + * The default {@link ShuffleOrder} implementation for random shuffle order. + */ + class DefaultShuffleOrder implements ShuffleOrder { + + private final Random random; + private final int[] shuffled; + private final int[] indexInShuffled; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public DefaultShuffleOrder(int length) { + this(length, new Random()); + } + + /** + * Creates an instance with a specified length and the specified random seed. Shuffle orders of + * the same length initialized with the same random seed are guaranteed to be equal. + * + * @param length The length of the shuffle order. + * @param randomSeed A random seed. + */ + public DefaultShuffleOrder(int length, long randomSeed) { + this(length, new Random(randomSeed)); + } + + private DefaultShuffleOrder(int length, Random random) { + this(createShuffledList(length, random), random); + } + + private DefaultShuffleOrder(int[] shuffled, Random random) { + this.shuffled = shuffled; + this.random = random; + this.indexInShuffled = new int[shuffled.length]; + for (int i = 0; i < shuffled.length; i++) { + indexInShuffled[shuffled[i]] = i; + } + } + + @Override + public int getLength() { + return shuffled.length; + } + + @Override + public int getNextIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + int[] insertionPoints = new int[insertionCount]; + int[] insertionValues = new int[insertionCount]; + for (int i = 0; i < insertionCount; i++) { + insertionPoints[i] = random.nextInt(shuffled.length + 1); + int swapIndex = random.nextInt(i + 1); + insertionValues[i] = insertionValues[swapIndex]; + insertionValues[swapIndex] = i + insertionIndex; + } + Arrays.sort(insertionPoints); + int[] newShuffled = new int[shuffled.length + insertionCount]; + int indexInOldShuffled = 0; + int indexInInsertionList = 0; + for (int i = 0; i < shuffled.length + insertionCount; i++) { + if (indexInInsertionList < insertionCount + && indexInOldShuffled == insertionPoints[indexInInsertionList]) { + newShuffled[i] = insertionValues[indexInInsertionList++]; + } else { + newShuffled[i] = shuffled[indexInOldShuffled++]; + if (newShuffled[i] >= insertionIndex) { + newShuffled[i] += insertionCount; + } + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + int[] newShuffled = new int[shuffled.length - 1]; + boolean foundRemovedElement = false; + for (int i = 0; i < shuffled.length; i++) { + if (shuffled[i] == removalIndex) { + foundRemovedElement = true; + } else { + newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex + ? shuffled[i] - 1 : shuffled[i]; + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + private static int[] createShuffledList(int length, Random random) { + int[] shuffled = new int[length]; + for (int i = 0; i < length; i++) { + int swapIndex = random.nextInt(i + 1); + shuffled[i] = shuffled[swapIndex]; + shuffled[swapIndex] = i; + } + return shuffled; + } + + } + + /** + * A {@link ShuffleOrder} implementation which does not shuffle. + */ + final class UnshuffledShuffleOrder implements ShuffleOrder { + + private final int length; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public UnshuffledShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return ++index < length ? index : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return --index >= 0 ? index : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new UnshuffledShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + return new UnshuffledShuffleOrder(length - 1); + } + + } + + /** + * Returns length of shuffle order. + */ + int getLength(); + + /** + * Returns the next index in the shuffle order. + * + * @param index An index. + * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last + * element. + */ + int getNextIndex(int index); + + /** + * Returns the previous index in the shuffle order. + * + * @param index An index. + * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first + * element. + */ + int getPreviousIndex(int index); + + /** + * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getLastIndex(); + + /** + * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getFirstIndex(); + + /** + * Return a copy of the shuffle order with newly inserted elements. + * + * @param insertionIndex The index in the unshuffled order at which elements are inserted. + * @param insertionCount The number of elements inserted at {@code insertionIndex}. + * @return A copy of this {@link ShuffleOrder} with newly inserted elements. + */ + ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount); + + /** + * Return a copy of the shuffle order with one element removed. + * + * @param removalIndex The index of the element in the unshuffled order which is to be removed. + * @return A copy of this {@link ShuffleOrder} without the removed element. + */ + ShuffleOrder cloneAndRemove(int removalIndex); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 3435c01eeb..b19f398d86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -235,10 +235,12 @@ import java.util.Arrays; } @Override - public void skipData(long positionUs) { - if (positionUs > 0) { + public int skipData(long positionUs) { + if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_END_OF_STREAM; + return 1; } + return 0; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java index 0fc3d5881e..f2609a0ffd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java @@ -160,6 +160,7 @@ public class ChunkSampleStream implements SampleStream, S * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. */ + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -185,8 +186,8 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true, - positionUs < getNextLoadPositionUs()); + boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, + positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); if (seekInsideBuffer) { // We succeeded. Discard samples and corresponding chunks prior to the seek position. discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); @@ -266,13 +267,19 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipData(long positionUs) { + public int skipData(long positionUs) { + int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { primarySampleQueue.advanceToEnd(); + skipCount = primarySampleQueue.advanceToEnd(); } else { - primarySampleQueue.advanceTo(positionUs, true, true); + skipCount = primarySampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } } primarySampleQueue.discardToRead(); + return skipCount; } // Loader.Callback implementation. @@ -470,11 +477,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipData(long positionUs) { + public int skipData(long positionUs) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 8fd70f7a67..030f0cdbb0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -811,43 +811,43 @@ public final class Cea708Decoder extends CeaDecoder { private static final int PEN_OFFSET_NORMAL = 1; // The window style properties are specified in the CEA-708 specification. - private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{ + private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] { JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, JUSTIFICATION_LEFT }; - private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] { DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_TOP_TO_BOTTOM }; - private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] { DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_RIGHT_TO_LEFT }; - private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{ + private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] { false, false, false, true, true, true, false }; - private static final int[] WINDOW_STYLE_FILL = new int[]{ + private static final int[] WINDOW_STYLE_FILL = new int[] { COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK }; // The pen style properties are specified in the CEA-708 specification. - private static final int[] PEN_STYLE_FONT_STYLE = new int[]{ + private static final int[] PEN_STYLE_FONT_STYLE = new int[] { PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS }; - private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{ + private static final int[] PEN_STYLE_EDGE_TYPE = new int[] { BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, BORDER_AND_EDGE_TYPE_UNIFORM }; - private static final int[] PEN_STYLE_BACKGROUND = new int[]{ + private static final int[] PEN_STYLE_BACKGROUND = new int[] { COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 58cc70d68d..10bc298579 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -34,9 +34,9 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; @@ -176,14 +176,14 @@ import javax.crypto.spec.SecretKeySpec; /** Removes empty {@link CachedContent} instances from index. */ public void removeEmpty() { - LinkedList cachedContentToBeRemoved = new LinkedList<>(); + ArrayList cachedContentToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : keyToContent.values()) { if (cachedContent.isEmpty()) { cachedContentToBeRemoved.add(cachedContent.key); } } - for (String key : cachedContentToBeRemoved) { - removeEmpty(key); + for (int i = 0; i < cachedContentToBeRemoved.size(); i++) { + removeEmpty(cachedContentToBeRemoved.get(i)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 2da6ba759b..62bd2783b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -22,7 +22,6 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; @@ -308,7 +307,7 @@ public final class SimpleCache implements Cache { * no longer exist. */ private void removeStaleSpansAndCachedContents() throws CacheException { - LinkedList spansToBeRemoved = new LinkedList<>(); + ArrayList spansToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : index.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { if (!span.file.exists()) { @@ -316,9 +315,9 @@ public final class SimpleCache implements Cache { } } } - for (CacheSpan span : spansToBeRemoved) { + for (int i = 0; i < spansToBeRemoved.size(); i++) { // Remove span but not CachedContent to prevent multiple index.store() calls. - removeSpan(span, false); + removeSpan(spansToBeRemoved.get(i), false); } index.removeEmpty(); index.store(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index f2e30d981b..e85c07fba9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.util; import android.support.annotation.NonNull; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 0093c3b826..0514d9dbdc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import java.util.ArrayList; import java.util.List; @@ -83,11 +84,27 @@ public final class CodecSpecificDataUtil { /** * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * - * @param audioSpecificConfig The AudioSpecificConfig to parse. + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { - ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); + public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) + throws ParserException { + return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false); + } + + /** + * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The + * position is advanced to the end of the AudioSpecificConfig. + * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for + * knowing the length of the configuration payload. + * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. + */ + public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray, + boolean forceReadToEnd) throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -104,6 +121,41 @@ public final class CodecSpecificDataUtil { channelConfiguration = bitArray.readBits(4); } } + + if (forceReadToEnd) { + switch (audioObjectType) { + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); + break; + default: + throw new ParserException("Unsupported audio object type: " + audioObjectType); + } + switch (audioObjectType) { + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + int epConfig = bitArray.readBits(2); + if (epConfig == 2 || epConfig == 3) { + throw new ParserException("Unsupported epConfig: " + epConfig); + } + break; + } + } + // For supported containers, bits_to_decode() is always 0. int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); return Pair.create(sampleRate, channelCount); @@ -269,4 +321,32 @@ public final class CodecSpecificDataUtil { return samplingFrequency; } + private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType, + int channelConfiguration) { + bitArray.skipBits(1); // frameLengthFlag. + boolean dependsOnCoreDecoder = bitArray.readBit(); + if (dependsOnCoreDecoder) { + bitArray.skipBits(14); // coreCoderDelay. + } + boolean extensionFlag = bitArray.readBit(); + if (channelConfiguration == 0) { + throw new UnsupportedOperationException(); // TODO: Implement programConfigElement(); + } + if (audioObjectType == 6 || audioObjectType == 20) { + bitArray.skipBits(3); // layerNr. + } + if (extensionFlag) { + if (audioObjectType == 22) { + bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11). + } + if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20 + || audioObjectType == 23) { + // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag, + // aacSpectralDataResilienceFlag. + bitArray.skipBits(3); + } + bitArray.skipBits(1); // extensionFlag3. + } + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 199ceff892..fdee7fb5e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -174,6 +174,43 @@ public final class ParsableBitArray { return returnValue; } + /** + * Reads {@code numBits} bits into {@code buffer}. + * + * @param buffer The array into which the read data should be written. The trailing + * {@code numBits % 8} bits are written into the most significant bits of the last modified + * {@code buffer} byte. The remaining ones are unmodified. + * @param offset The offset in {@code buffer} at which the read data should be written. + * @param numBits The number of bits to read. + */ + public void readBits(byte[] buffer, int offset, int numBits) { + // Whole bytes. + int to = offset + (numBits >> 3) /* numBits / 8 */; + for (int i = offset; i < to; i++) { + buffer[i] = (byte) (data[byteOffset++] << bitOffset); + buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + } + // Trailing bits. + int bitsLeft = numBits & 7 /* numBits % 8 */; + if (bitsLeft == 0) { + return; + } + buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten. + if (bitOffset + bitsLeft > 8) { + // We read the rest of data[byteOffset] and increase byteOffset. + buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset); + bitOffset -= 8; + } + bitOffset += bitsLeft; + int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset); + buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft)); + if (bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + assertValidOffset(); + } + /** * Aligns the position to the next byte boundary. Does nothing if the position is already aligned. */ diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java index bac1c272e8..7a1c78b2e8 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/DashUtilTest.java @@ -76,7 +76,7 @@ public final class DashUtilTest extends TestCase { private static DrmInitData newDrmInitData() { return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", - new byte[]{1, 4, 7, 0, 3, 6})); + new byte[] {1, 4, 7, 0, 3, 6})); } } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index ac77725ca5..5471eacec6 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -35,6 +35,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java index 450644f60f..e423a682f3 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStream.java @@ -50,8 +50,8 @@ import java.io.IOException; } @Override - public void skipData(long positionUs) { - sampleStreamWrapper.skipData(group, positionUs); + public int skipData(long positionUs) { + return sampleStreamWrapper.skipData(group, positionUs); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 0b6d1863bd..00a3cd4a85 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -229,7 +229,7 @@ import java.util.LinkedList; // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED && sampleQueue.getReadIndex() != 0; } } @@ -320,6 +320,7 @@ import java.util.LinkedList; return true; } + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -402,12 +403,13 @@ import java.util.LinkedList; lastSeekPositionUs); } - /* package */ void skipData(int trackGroupIndex, long positionUs) { + /* package */ int skipData(int trackGroupIndex, long positionUs) { SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } @@ -760,7 +762,8 @@ import java.util.LinkedList; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) + != SampleQueue.ADVANCE_FAILED; // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java index b38763f7e8..04192def9d 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylist.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -108,6 +109,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ? Collections.unmodifiableList(muxedCaptionFormats) : null; } + /** + * Returns a copy of this playlist which includes only the renditions identified by the given + * urls. + * + * @param renditionUrls List of rendition urls. + * @return A copy of this playlist which includes only the renditions identified by the given + * urls. + */ + public HlsMasterPlaylist copy(List renditionUrls) { + return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls), + copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls), + muxedAudioFormat, muxedCaptionFormats); + } + /** * Creates a playlist with a single variant. * @@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } + private static List copyRenditionsList(List renditions, List urls) { + List copiedRenditions = new ArrayList<>(urls.size()); + for (int i = 0; i < renditions.size(); i++) { + HlsUrl rendition = renditions.get(i); + if (urls.contains(rendition.url)) { + copiedRenditions.add(rendition); + } + } + return copiedRenditions; + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index cb5e3465f8..060780eda2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -186,8 +186,9 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener return ""; } counters.ensureUpdated(); - return " rb:" + counters.renderedOutputBufferCount + return " sib:" + counters.skippedInputBufferCount + " sb:" + counters.skippedOutputBufferCount + + " rb:" + counters.renderedOutputBufferCount + " db:" + counters.droppedOutputBufferCount + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; } diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml new file mode 100644 index 0000000000..28ac6a5786 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000..52a805aac1 Binary files /dev/null and b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png differ diff --git a/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png new file mode 100644 index 0000000000..80ec43a119 Binary files /dev/null and b/library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png differ diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000..0924b2cb69 Binary files /dev/null and b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png differ diff --git a/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000..ede80c9341 Binary files /dev/null and b/library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png differ diff --git a/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000..4c5e141a3f Binary files /dev/null and b/library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png differ diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 103877f1e6..2510552c0c 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -25,4 +25,5 @@ "Herhaal alles" "Herhaal niks" "Herhaal een" + "Skommel" diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index 356566cb87..165b5eee62 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -25,4 +25,5 @@ "ሁሉንም ድገም" "ምንም አትድገም" "አንዱን ድገም" + "በው" diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 4bdbda061c..239f01be6b 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -25,4 +25,5 @@ "تكرار الكل" "عدم التكرار" "تكرار مقطع واحد" + "ترتيب عشوائي" diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml index 771335952f..1071cd5542 100644 --- a/library/ui/src/main/res/values-az-rAZ/strings.xml +++ b/library/ui/src/main/res/values-az-rAZ/strings.xml @@ -25,4 +25,5 @@ "Bütün təkrarlayın" "Təkrar bir" "Heç bir təkrar" + "Qarışdır" diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index 7c373b5b55..a9d35e5cb6 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -25,4 +25,5 @@ "Ponovi sve" "Ne ponavljaj nijednu" "Ponovi jednu" + "Pusti nasumično" diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml index 7790a7887f..69b24ad5e9 100644 --- a/library/ui/src/main/res/values-be-rBY/strings.xml +++ b/library/ui/src/main/res/values-be-rBY/strings.xml @@ -25,4 +25,5 @@ "Паўтарыць усё" "Паўтараць ні" "Паўтарыць адзін" + "Перамяшаць" diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index ce9e3d6943..e350479788 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -25,4 +25,5 @@ "Повтаряне на всички" "Без повтаряне" "Повтаряне на един елемент" + "Разбъркване" diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml index 5f8ebfa98e..446ef982a3 100644 --- a/library/ui/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui/src/main/res/values-bn-rBD/strings.xml @@ -25,4 +25,5 @@ "সবগুলির পুনরাবৃত্তি করুন" "একটিরও পুনরাবৃত্তি করবেন না" "একটির পুনরাবৃত্তি করুন" + "অদলবদল" diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml index ef47099760..186b1058d9 100644 --- a/library/ui/src/main/res/values-bs-rBA/strings.xml +++ b/library/ui/src/main/res/values-bs-rBA/strings.xml @@ -25,4 +25,5 @@ "Ponovite sve" "Ne ponavljaju" "Ponovite jedan" + "Izmiješaj" diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index a42fe3b9cb..fd76a8e08e 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -25,4 +25,5 @@ "Repeteix-ho tot" "No en repeteixis cap" "Repeteix-ne un" + "Reprodueix aleatòriament" diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 9c1e50ce27..087ab79c25 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -25,4 +25,5 @@ "Opakovat vše" "Neopakovat" "Opakovat jednu položku" + "Náhodně" diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 3ec132ebb7..0ae23ee288 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -25,4 +25,5 @@ "Gentag alle" "Gentag ingen" "Gentag en" + "Bland" diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index a1dc749864..37ca6c44ac 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -25,4 +25,5 @@ "Alle wiederholen" "Keinen Titel wiederholen" "Einen Titel wiederholen" + "Zufallsmix" diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 845011fe55..534192e185 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -25,4 +25,5 @@ "Επανάληψη όλων" "Καμία επανάληψη" "Επανάληψη ενός στοιχείου" + "Τυχαία αναπαραγωγή" diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index f2ec848fb6..e6cf3fc6f2 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "No repetir" "Repetir uno" + "Reproducir aleatoriamente" diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 116f064223..04e1ea038c 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "No repetir" "Repetir uno" + "Reproducción aleatoria" diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml index 153611ece4..004ec7e6c3 100644 --- a/library/ui/src/main/res/values-et-rEE/strings.xml +++ b/library/ui/src/main/res/values-et-rEE/strings.xml @@ -25,4 +25,5 @@ "Korda kõike" "Ära korda midagi" "Korda ühte" + "Esita juhuslikus järjekorras" diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml index 1128572d9a..6a3345303a 100644 --- a/library/ui/src/main/res/values-eu-rES/strings.xml +++ b/library/ui/src/main/res/values-eu-rES/strings.xml @@ -25,4 +25,5 @@ "Errepikatu guztiak" "Ez errepikatu" "Errepikatu bat" + "Erreproduzitu ausaz" diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index d6be77323b..101fcdbfb5 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -25,4 +25,5 @@ "تکرار همه" "تکرار هیچ‌کدام" "یک‌بار تکرار" + "پخش درهم" diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 10e4b0bbe3..92feb86683 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -25,4 +25,5 @@ "Toista kaikki" "Toista ei mitään" "Toista yksi" + "Toista satunnaisesti" diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index d8852b5d3f..45fc0a86f9 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -25,4 +25,5 @@ "Tout lire en boucle" "Aucune répétition" "Répéter un élément" + "Lecture aléatoire" diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index acf3670fa4..82b5a40626 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -25,4 +25,5 @@ "Tout lire en boucle" "Ne rien lire en boucle" "Lire en boucle un élément" + "Lire en mode aléatoire" diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml index 81b854cafe..7062d8d023 100644 --- a/library/ui/src/main/res/values-gl-rES/strings.xml +++ b/library/ui/src/main/res/values-gl-rES/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "Non repetir" "Repetir un" + "Reprodución aleatoria" diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml index 6d51c29f97..ed78b1ee30 100644 --- a/library/ui/src/main/res/values-gu-rIN/strings.xml +++ b/library/ui/src/main/res/values-gu-rIN/strings.xml @@ -25,4 +25,5 @@ "બધા પુનરાવર્તન કરો" "કંઈ પુનરાવર્તન કરો" "એક પુનરાવર્તન કરો" + "શફલ કરો" diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index eadb0519df..ec624b1f35 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -25,4 +25,5 @@ "सभी को दोहराएं" "कुछ भी न दोहराएं" "एक दोहराएं" + "शफ़ल करें" diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index cb49965640..7cb23e11dd 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -25,4 +25,5 @@ "Ponovi sve" "Bez ponavljanja" "Ponovi jedno" + "Reproduciraj nasumično" diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index 43ac8f51ff..cf3d34c88f 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -25,4 +25,5 @@ "Összes ismétlése" "Nincs ismétlés" "Egy ismétlése" + "Véletlenszerű lejátszás" diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml index 3b09f9a507..13a489baf5 100644 --- a/library/ui/src/main/res/values-hy-rAM/strings.xml +++ b/library/ui/src/main/res/values-hy-rAM/strings.xml @@ -25,4 +25,5 @@ "կրկնել այն ամենը" "Չկրկնել" "Կրկնել մեկը" + "Խառնել" diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 928be5945a..09b05815e6 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -25,4 +25,5 @@ "Ulangi Semua" "Jangan Ulangi" "Ulangi Satu" + "Acak" diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml index 75be2aeb17..12c4632cdf 100644 --- a/library/ui/src/main/res/values-is-rIS/strings.xml +++ b/library/ui/src/main/res/values-is-rIS/strings.xml @@ -25,4 +25,5 @@ "Endurtaka allt" "Endurtaka ekkert" "Endurtaka eitt" + "Stokka" diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 59117a6b75..aea20db82e 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -25,4 +25,5 @@ "Ripeti tutti" "Non ripetere nessuno" "Ripeti uno" + "Riproduci casualmente" diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index 347b137cf2..dd973af50b 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -25,4 +25,5 @@ "חזור על הכל" "אל תחזור על כלום" "חזור על פריט אחד" + "ערבב" diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index cf2cc49b67..d6ce751d5c 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -25,4 +25,5 @@ "全曲を繰り返し" "繰り返しなし" "1曲を繰り返し" + "シャッフル" diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml index 75da8dde18..252e52f151 100644 --- a/library/ui/src/main/res/values-ka-rGE/strings.xml +++ b/library/ui/src/main/res/values-ka-rGE/strings.xml @@ -25,4 +25,5 @@ "გამეორება ყველა" "გაიმეორეთ არცერთი" "გაიმეორეთ ერთი" + "არეულად დაკვრა" diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml index b1ab22ecf6..43eb3dd030 100644 --- a/library/ui/src/main/res/values-kk-rKZ/strings.xml +++ b/library/ui/src/main/res/values-kk-rKZ/strings.xml @@ -25,4 +25,5 @@ "Барлығын қайталау" "Ешқайсысын қайталамау" "Біреуін қайталау" + "Кездейсоқ ретпен ойнату" diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml index dfd9f7d863..653c9f051d 100644 --- a/library/ui/src/main/res/values-km-rKH/strings.xml +++ b/library/ui/src/main/res/values-km-rKH/strings.xml @@ -25,4 +25,5 @@ "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" "មិន​ធ្វើ​ឡើង​វិញ" "ធ្វើ​​ឡើងវិញ​ម្ដង" + "ច្របល់" diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml index 868af17a65..7368fc8ad3 100644 --- a/library/ui/src/main/res/values-kn-rIN/strings.xml +++ b/library/ui/src/main/res/values-kn-rIN/strings.xml @@ -25,4 +25,5 @@ "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" + "ಬೆರೆಸು" diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 89636ac8a0..99d4a2f9a4 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -25,4 +25,5 @@ "전체 반복" "반복 안함" "한 항목 반복" + "셔플" diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml index 15fd50468a..9b903a124e 100644 --- a/library/ui/src/main/res/values-ky-rKG/strings.xml +++ b/library/ui/src/main/res/values-ky-rKG/strings.xml @@ -25,4 +25,5 @@ "Баарын кайталоо" "Эч бирин кайталабоо" "Бирөөнү кайталоо" + "Аралаштыруу" diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml index 405d0c64fe..702cd54396 100644 --- a/library/ui/src/main/res/values-lo-rLA/strings.xml +++ b/library/ui/src/main/res/values-lo-rLA/strings.xml @@ -25,4 +25,5 @@ "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" "ຫຼິ້ນ​ຊ້ຳ" + "ຫຼີ້ນແບບສຸ່ມ" diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index bd7d4142fc..d6073f42e3 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -25,4 +25,5 @@ "Kartoti viską" "Nekartoti nieko" "Kartoti vieną" + "Maišyti" diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index c2ebc70cbd..64393d679a 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -25,4 +25,5 @@ "Atkārtot visu" "Neatkārtot nevienu" "Atkārtot vienu" + "Atskaņot jauktā secībā" diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml index 14ce7111a4..60858df8b1 100644 --- a/library/ui/src/main/res/values-mk-rMK/strings.xml +++ b/library/ui/src/main/res/values-mk-rMK/strings.xml @@ -25,4 +25,5 @@ "Повтори ги сите" "Не повторувај ниту една" "Повтори една" + "По случаен избор" diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml index 17fe7a1655..4e5eddb93e 100644 --- a/library/ui/src/main/res/values-ml-rIN/strings.xml +++ b/library/ui/src/main/res/values-ml-rIN/strings.xml @@ -25,4 +25,5 @@ "എല്ലാം ആവർത്തിക്കുക" "ഒന്നും ആവർത്തിക്കരുത്" "ഒന്ന് ആവർത്തിക്കുക" + "ഷഫിൾ ചെയ്യുക" diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml index bf9a7e03bf..4ab26a7f62 100644 --- a/library/ui/src/main/res/values-mn-rMN/strings.xml +++ b/library/ui/src/main/res/values-mn-rMN/strings.xml @@ -25,4 +25,5 @@ "Бүгдийг давтах" "Алийг нь ч давтахгүй" "Нэгийг давтах" + "Холих" diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml index df4ac9de6b..7869355b59 100644 --- a/library/ui/src/main/res/values-mr-rIN/strings.xml +++ b/library/ui/src/main/res/values-mr-rIN/strings.xml @@ -25,4 +25,5 @@ "सर्व पुनरावृत्ती करा" "काहीही पुनरावृत्ती करू नका" "एक पुनरावृत्ती करा" + "शफल करा" diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml index 33dfcb40f0..fdde3de079 100644 --- a/library/ui/src/main/res/values-ms-rMY/strings.xml +++ b/library/ui/src/main/res/values-ms-rMY/strings.xml @@ -25,4 +25,5 @@ "Ulang semua" "Tiada ulangan" "Ulangan" + "Rombak" diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml index b4ea5b1155..3d7918d953 100644 --- a/library/ui/src/main/res/values-my-rMM/strings.xml +++ b/library/ui/src/main/res/values-my-rMM/strings.xml @@ -25,4 +25,5 @@ "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" "ထပ်တလဲလဲမဖွင့်ရန်" "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" + "မွှေနှောက်ဖွင့်ရန်" diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 679bf1134c..370c759b84 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -25,4 +25,5 @@ "Gjenta alle" "Ikke gjenta noen" "Gjenta én" + "Spill av i tilfeldig rekkefølge" diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml index 43730c1880..19f43d0392 100644 --- a/library/ui/src/main/res/values-ne-rNP/strings.xml +++ b/library/ui/src/main/res/values-ne-rNP/strings.xml @@ -25,4 +25,5 @@ "सबै दोहोर्याउनुहोस्" "कुनै पनि नदोहोर्याउनुहोस्" "एउटा दोहोर्याउनुहोस्" + "मिसाउनुहोस्" diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 6383c977fc..a67ab2968c 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -25,4 +25,5 @@ "Alles herhalen" "Niet herhalen" "Eén herhalen" + "Shuffle" diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml index ddf60b0394..6250b90514 100644 --- a/library/ui/src/main/res/values-pa-rIN/strings.xml +++ b/library/ui/src/main/res/values-pa-rIN/strings.xml @@ -25,4 +25,5 @@ "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" "ਇੱਕ ਦੁਹਰਾਓ" + "ਸ਼ੱਫਲ" diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 113c568f85..ff1d77fdd5 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -25,4 +25,5 @@ "Powtórz wszystkie" "Nie powtarzaj" "Powtórz jeden" + "Odtwarzaj losowo" diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml index 87c54358ba..86a91b0677 100644 --- a/library/ui/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui/src/main/res/values-pt-rBR/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir um" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index ca34afec3c..5a7144e36b 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir um" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 2fc3191738..8441e4e1cc 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir uma" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 0b2ce540f7..6b8644e30a 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -25,4 +25,5 @@ "Repetați toate" "Repetați niciuna" "Repetați unul" + "Redați aleatoriu" diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index 1d179e028c..51d11d6371 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -25,4 +25,5 @@ "Повторять все" "Не повторять" "Повторять один элемент" + "Перемешать" diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml index bc37d98eed..eb8453b156 100644 --- a/library/ui/src/main/res/values-si-rLK/strings.xml +++ b/library/ui/src/main/res/values-si-rLK/strings.xml @@ -25,4 +25,5 @@ "සියලු නැවත" "කිසිවක් නැවත" "නැවත නැවත එක්" + "කලවම් කරන්න" diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index a6ea26bdf0..2428dbdcce 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -25,4 +25,5 @@ "Opakovať všetko" "Neopakovať" "Opakovať jednu položku" + "Náhodne prehrávať" diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 39813fa385..8ed731b0d3 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -25,4 +25,5 @@ "Ponovi vse" "Ne ponovi" "Ponovi eno" + "Naključno predvajaj" diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml index 0bdc2e5f84..e2d209e10b 100644 --- a/library/ui/src/main/res/values-sq-rAL/strings.xml +++ b/library/ui/src/main/res/values-sq-rAL/strings.xml @@ -25,4 +25,5 @@ "Përsërit të gjithë" "Përsëritni asnjë" "Përsëritni një" + "Përziej" diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 0d54de5f6a..8e43a03079 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -25,4 +25,5 @@ "Понови све" "Понављање је искључено" "Понови једну" + "Пусти насумично" diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index 0f7f16f91d..5ff1100632 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -25,4 +25,5 @@ "Upprepa alla" "Upprepa inga" "Upprepa en" + "Blanda" diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index b48af88659..d1d5978f9c 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -25,4 +25,5 @@ "Rudia zote" "Usirudie Yoyote" "Rudia Moja" + "Changanya" diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml index 3dd64f52f7..43a925aa2e 100644 --- a/library/ui/src/main/res/values-ta-rIN/strings.xml +++ b/library/ui/src/main/res/values-ta-rIN/strings.xml @@ -25,4 +25,5 @@ "அனைத்தையும் மீண்டும் இயக்கு" "எதையும் மீண்டும் இயக்காதே" "ஒன்றை மட்டும் மீண்டும் இயக்கு" + "குலை" diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml index daf337a931..8541a44553 100644 --- a/library/ui/src/main/res/values-te-rIN/strings.xml +++ b/library/ui/src/main/res/values-te-rIN/strings.xml @@ -25,4 +25,5 @@ "అన్నీ పునరావృతం చేయి" "ఏదీ పునరావృతం చేయవద్దు" "ఒకదాన్ని పునరావృతం చేయి" + "షఫుల్ చేయి" diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index ff89b8d5f5..cd97712b67 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -25,4 +25,5 @@ "เล่นซ้ำทั้งหมด" "ไม่เล่นซ้ำ" "เล่นซ้ำรายการเดียว" + "สุ่มเพลง" diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 89cf2ef400..e8cb87acdd 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -25,4 +25,5 @@ "Ulitin Lahat" "Walang Uulitin" "Ulitin ang Isa" + "I-shuffle" diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 87dba7204c..cd1bfc5444 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -25,4 +25,5 @@ "Tümünü Tekrarla" "Hiçbirini Tekrarlama" "Birini Tekrarla" + "Karıştır" diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 1fdfe2bce5..1b0278ae94 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -25,4 +25,5 @@ "Повторити все" "Не повторювати" "Повторити один елемент" + "Перемішати" diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml index 956374b26a..f253e56c00 100644 --- a/library/ui/src/main/res/values-ur-rPK/strings.xml +++ b/library/ui/src/main/res/values-ur-rPK/strings.xml @@ -25,4 +25,5 @@ "سبھی کو دہرائیں" "کسی کو نہ دہرائیں" "ایک کو دہرائیں" + "شفل کریں" diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml index 286d4d01ab..a322690b2d 100644 --- a/library/ui/src/main/res/values-uz-rUZ/strings.xml +++ b/library/ui/src/main/res/values-uz-rUZ/strings.xml @@ -25,4 +25,5 @@ "Barchasini takrorlash" "Takrorlamaslik" "Bir marta takrorlash" + "Tasodifiy tartibda" diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 4dea58d494..cff19eca7e 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -25,4 +25,5 @@ "Lặp lại tất cả" "Không lặp lại" "Lặp lại một mục" + "Trộn bài" diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index e15d84e777..cf3fe5e88b 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -25,4 +25,5 @@ "重复播放全部" "不重复播放" "重复播放单个视频" + "随机播放" diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index ba793e98a8..78fe4ad995 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -25,4 +25,5 @@ "重複播放所有媒體項目" "不重複播放任何媒體項目" "重複播放一個媒體項目" + "隨機播放" diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index bf3364d5cf..3632742904 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -25,4 +25,5 @@ "重複播放所有媒體項目" "不重複播放" "重複播放單一媒體項目" + "隨機播放" diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index d7bebaaa2a..42dd59c97f 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -25,4 +25,5 @@ "Phinda konke" "Ungaphindi lutho" "Phida okukodwa" + "Shova" diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index b16b1729da..b90d2329b3 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -28,6 +28,7 @@ + diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index c5d11eeadb..ee8cd78be7 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -24,4 +24,5 @@ Repeat none Repeat one Repeat all + Shuffle diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index a67cffe420..4ef8971ccd 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -51,4 +51,9 @@ @string/exo_controls_pause_description + + diff --git a/settings.gradle b/settings.gradle index fb31055f5e..766d46bbae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,7 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'playbacktests' -project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demo') +project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index b1c6f081cf..bbb694d6d6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -15,11 +15,20 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; /** * Base class for actions to perform during playback tests. @@ -39,21 +48,41 @@ public abstract class Action { } /** - * Executes the action. + * Executes the action and schedules the next. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. * @param surface The surface to use when applying actions. + * @param handler The handler to use to pass to the next action. + * @param nextAction The next action to schedule immediately after this action finished. */ - public final void doAction(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + public final void doActionAndScheduleNext(SimpleExoPlayer player, + MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { Log.i(tag, description); - doActionImpl(player, trackSelector, surface); + doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction); } /** - * Called by {@link #doAction(SimpleExoPlayer, MappingTrackSelector, Surface)} do perform the - * action. + * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, MappingTrackSelector, Surface, + * Handler, ActionNode)} to perform the action and to schedule the next action node. + * + * @param player The player to which the action should be applied. + * @param trackSelector The track selector to which the action should be applied. + * @param surface The surface to use when applying actions. + * @param handler The handler to use to pass to the next action. + * @param nextAction The next action to schedule immediately after this action finished. + */ + protected void doActionAndScheduleNextImpl(SimpleExoPlayer player, + MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + doActionImpl(player, trackSelector, surface); + if (nextAction != null) { + nextAction.schedule(player, trackSelector, surface, handler); + } + } + + /** + * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, MappingTrackSelector, Surface, + * Handler, ActionNode)} to perform the action. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. @@ -63,7 +92,7 @@ public abstract class Action { Surface surface); /** - * Calls {@link ExoPlayer#seekTo(long)}. + * Calls {@link Player#seekTo(long)}. */ public static final class Seek extends Action { @@ -87,7 +116,7 @@ public abstract class Action { } /** - * Calls {@link ExoPlayer#stop()}. + * Calls {@link Player#stop()}. */ public static final class Stop extends Action { @@ -107,7 +136,7 @@ public abstract class Action { } /** - * Calls {@link ExoPlayer#setPlayWhenReady(boolean)}. + * Calls {@link Player#setPlayWhenReady(boolean)}. */ public static final class SetPlayWhenReady extends Action { @@ -197,5 +226,206 @@ public abstract class Action { } + /** + * Calls {@link ExoPlayer#prepare(MediaSource)}. + */ + public static final class PrepareSource extends Action { + + private final MediaSource mediaSource; + private final boolean resetPosition; + private final boolean resetState; + + /** + * @param tag A tag to use for logging. + */ + public PrepareSource(String tag, MediaSource mediaSource) { + this(tag, mediaSource, true, true); + } + + /** + * @param tag A tag to use for logging. + */ + public PrepareSource(String tag, MediaSource mediaSource, boolean resetPosition, + boolean resetState) { + super(tag, "PrepareSource"); + this.mediaSource = mediaSource; + this.resetPosition = resetPosition; + this.resetState = resetState; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.prepare(mediaSource, resetPosition, resetState); + } + + } + + /** + * Calls {@link Player#setRepeatMode(int)}. + */ + public static final class SetRepeatMode extends Action { + + private final @Player.RepeatMode int repeatMode; + + /** + * @param tag A tag to use for logging. + */ + public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { + super(tag, "SetRepeatMode:" + repeatMode); + this.repeatMode = repeatMode; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setRepeatMode(repeatMode); + } + + } + + /** + * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. + */ + public static final class WaitForTimelineChanged extends Action { + + private final Timeline expectedTimeline; + + /** + * @param tag A tag to use for logging. + */ + public WaitForTimelineChanged(String tag, Timeline expectedTimeline) { + super(tag, "WaitForTimelineChanged"); + this.expectedTimeline = expectedTimeline; + } + + @Override + protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + final ActionNode nextAction) { + PlayerListener listener = new PlayerListener() { + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (timeline.equals(expectedTimeline)) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }; + player.addListener(listener); + if (player.getCurrentTimeline().equals(expectedTimeline)) { + player.removeListener(listener); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + // Not triggered. + } + + } + + /** + * Waits for {@link Player.EventListener#onPositionDiscontinuity()}. + */ + public static final class WaitForPositionDiscontinuity extends Action { + + /** + * @param tag A tag to use for logging. + */ + public WaitForPositionDiscontinuity(String tag) { + super(tag, "WaitForPositionDiscontinuity"); + } + + @Override + protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + final ActionNode nextAction) { + player.addListener(new PlayerListener() { + @Override + public void onPositionDiscontinuity() { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + }); + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + // Not triggered. + } + + } + + /** + * Calls {@link Runnable#run()}. + */ + public static final class ExecuteRunnable extends Action { + + private final Runnable runnable; + + /** + * @param tag A tag to use for logging. + */ + public ExecuteRunnable(String tag, Runnable runnable) { + super(tag, "ExecuteRunnable"); + this.runnable = runnable; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + runnable.run(); + } + + } + + /** Listener implementation used for overriding. Does nothing. */ + private static class PlayerListener implements Player.EventListener { + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + + @Override + public void onLoadingChanged(boolean isLoading) { + + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + + } + + @Override + public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) { + + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + + } + + @Override + public void onPositionDiscontinuity() { + + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + + } + + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 66f7ebca95..4392dd9d3f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -18,13 +18,22 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; +import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; +import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; +import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; +import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; +import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -179,6 +188,63 @@ public final class ActionSchedule { return apply(new SetVideoSurface(tag)); } + /** + * Schedules a new source preparation action to be executed. + * + * @return The builder, for convenience. + */ + public Builder prepareSource(MediaSource mediaSource) { + return apply(new PrepareSource(tag, mediaSource)); + } + + /** + * Schedules a new source preparation action to be executed. + * @see ExoPlayer#prepare(MediaSource, boolean, boolean). + * + * @return The builder, for convenience. + */ + public Builder prepareSource(MediaSource mediaSource, boolean resetPosition, + boolean resetState) { + return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); + } + + /** + * Schedules a repeat mode setting action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setRepeatMode(@Player.RepeatMode int repeatMode) { + return apply(new SetRepeatMode(tag, repeatMode)); + } + + /** + * Schedules a delay until the timeline changed to a specified expected timeline. + * + * @param expectedTimeline The expected timeline to wait for. + * @return The builder, for convenience. + */ + public Builder waitForTimelineChanged(Timeline expectedTimeline) { + return apply(new WaitForTimelineChanged(tag, expectedTimeline)); + } + + /** + * Schedules a delay until the next position discontinuity. + * + * @return The builder, for convenience. + */ + public Builder waitForPositionDiscontinuity() { + return apply(new WaitForPositionDiscontinuity(tag)); + } + + /** + * Schedules a {@link Runnable} to be executed. + * + * @return The builder, for convenience. + */ + public Builder executeRunnable(Runnable runnable) { + return apply(new ExecuteRunnable(tag, runnable)); + } + public ActionSchedule build() { return new ActionSchedule(rootNode); } @@ -195,7 +261,7 @@ public final class ActionSchedule { /** * Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. */ - private static final class ActionNode implements Runnable { + /* package */ static final class ActionNode implements Runnable { private final Action action; private final long delayMs; @@ -257,10 +323,7 @@ public final class ActionSchedule { @Override public void run() { - action.doAction(player, trackSelector, surface); - if (next != null) { - next.schedule(player, trackSelector, surface, mainHandler); - } + action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { mainHandler.postDelayed(this, repeatIntervalMs); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index b61b484e32..77e197515b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.ConditionVariable; import android.os.Handler; import android.os.SystemClock; import android.util.Log; @@ -72,6 +73,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; private final DecoderCounters audioDecoderCounters; + private final ConditionVariable testFinished; private ActionSchedule pendingSchedule; private Handler actionHandler; @@ -81,7 +83,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, private ExoPlaybackException playerError; private Player.EventListener playerEventListener; private boolean playerWasPrepared; - private boolean playerFinished; + private boolean playing; private long totalPlayingTimeMs; private long lastPlayingStartTimeMs; @@ -114,8 +116,9 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, this.tag = tag; this.expectedPlayingTimeMs = expectedPlayingTimeMs; this.failOnPlayerError = failOnPlayerError; - videoDecoderCounters = new DecoderCounters(); - audioDecoderCounters = new DecoderCounters(); + this.testFinished = new ConditionVariable(); + this.videoDecoderCounters = new DecoderCounters(); + this.audioDecoderCounters = new DecoderCounters(); } /** @@ -169,16 +172,13 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, } @Override - public final boolean canStop() { - return playerFinished; + public final boolean blockUntilStopped(long timeoutMs) { + return testFinished.block(timeoutMs); } @Override - public final void onStop() { - actionHandler.removeCallbacksAndMessages(null); - sourceDurationMs = player.getDuration(); - player.release(); - player = null; + public final boolean forceStop() { + return stopTest(); } @Override @@ -219,7 +219,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { - playerFinished = true; + stopTest(); } boolean playing = playWhenReady && playbackState == Player.STATE_READY; if (!this.playing && playing) { @@ -334,6 +334,25 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, // Internal logic + private boolean stopTest() { + if (player == null) { + return false; + } + actionHandler.removeCallbacksAndMessages(null); + sourceDurationMs = player.getDuration(); + player.release(); + player = null; + // We post opening of the finished condition so that any events posted to the main thread as a + // result of player.release() are guaranteed to be handled before the test returns. + actionHandler.post(new Runnable() { + @Override + public void run() { + testFinished.open(); + } + }); + return true; + } + protected DrmSessionManager buildDrmSessionManager(String userAgent) { // Do nothing. Interested subclasses may override. return null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java new file mode 100644 index 0000000000..2bfef0b4ab --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -0,0 +1,379 @@ +/* + * 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.os.Handler; +import android.os.HandlerThread; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.text.TextRenderer.Output; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.LinkedList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.Assert; + +/** + * Helper class to run an ExoPlayer test. + */ +public final class ExoPlayerTestRunner implements Player.EventListener { + + /** + * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for + * unset test properties. + */ + public static final class Builder { + + /** + * Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own + * {@link HandlerThread}. + */ + public interface PlayerFactory { + + SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, LoadControl loadControl); + + } + + public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, + MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, + null, null); + public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, + MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + + private PlayerFactory playerFactory; + private Timeline timeline; + private Object manifest; + private MediaSource mediaSource; + private MappingTrackSelector trackSelector; + private LoadControl loadControl; + private Format[] supportedFormats; + private Renderer[] renderers; + private RenderersFactory renderersFactory; + private ActionSchedule actionSchedule; + private Player.EventListener eventListener; + + public Builder setTimeline(Timeline timeline) { + Assert.assertNull(mediaSource); + this.timeline = timeline; + return this; + } + + public Builder setManifest(Object manifest) { + Assert.assertNull(mediaSource); + this.manifest = manifest; + return this; + } + + /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */ + public Builder setMediaSource(MediaSource mediaSource) { + Assert.assertNull(timeline); + Assert.assertNull(manifest); + this.mediaSource = mediaSource; + return this; + } + + public Builder setTrackSelector(MappingTrackSelector trackSelector) { + this.trackSelector = trackSelector; + return this; + } + + public Builder setLoadControl(LoadControl loadControl) { + this.loadControl = loadControl; + return this; + } + + public Builder setSupportedFormats(Format... supportedFormats) { + this.supportedFormats = supportedFormats; + return this; + } + + public Builder setRenderers(Renderer... renderers) { + Assert.assertNull(renderersFactory); + this.renderers = renderers; + return this; + } + + /** Replaces {@link #setRenderers(Renderer...)}. */ + public Builder setRenderersFactory(RenderersFactory renderersFactory) { + Assert.assertNull(renderers); + this.renderersFactory = renderersFactory; + return this; + } + + public Builder setExoPlayer(PlayerFactory playerFactory) { + this.playerFactory = playerFactory; + return this; + } + + public Builder setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public Builder setEventListener(Player.EventListener eventListener) { + this.eventListener = eventListener; + return this; + } + + public ExoPlayerTestRunner build() { + if (supportedFormats == null) { + supportedFormats = new Format[] { VIDEO_FORMAT }; + } + if (trackSelector == null) { + trackSelector = new DefaultTrackSelector(); + } + if (renderersFactory == null) { + if (renderers == null) { + renderers = new Renderer[] { new FakeRenderer(supportedFormats) }; + } + renderersFactory = new RenderersFactory() { + @Override + public Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, Output textRendererOutput, + MetadataRenderer.Output metadataRendererOutput) { + return renderers; + } + }; + } + if (loadControl == null) { + loadControl = new DefaultLoadControl(); + } + if (playerFactory == null) { + playerFactory = new PlayerFactory() { + @Override + public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, LoadControl loadControl) { + return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl); + } + }; + } + if (mediaSource == null) { + if (timeline == null) { + timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + } + mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); + } + return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, + loadControl, actionSchedule, eventListener); + } + } + + private final PlayerFactory playerFactory; + private final MediaSource mediaSource; + private final RenderersFactory renderersFactory; + private final MappingTrackSelector trackSelector; + private final LoadControl loadControl; + private final ActionSchedule actionSchedule; + private final Player.EventListener eventListener; + + private final HandlerThread playerThread; + private final Handler handler; + private final CountDownLatch endedCountDownLatch; + private final LinkedList timelines; + private final LinkedList manifests; + private final LinkedList periodIndices; + + private SimpleExoPlayer player; + private Exception exception; + private TrackGroupArray trackGroups; + private int positionDiscontinuityCount; + + private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, + RenderersFactory renderersFactory, MappingTrackSelector trackSelector, + LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener) { + this.playerFactory = playerFactory; + this.mediaSource = mediaSource; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.actionSchedule = actionSchedule; + this.eventListener = eventListener; + this.timelines = new LinkedList<>(); + this.manifests = new LinkedList<>(); + this.periodIndices = new LinkedList<>(); + this.endedCountDownLatch = new CountDownLatch(1); + this.playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + this.handler = new Handler(playerThread.getLooper()); + } + + // Called on the test thread to run the test. + + public ExoPlayerTestRunner start() { + handler.post(new Runnable() { + @Override + public void run() { + try { + player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start(player, trackSelector, null, handler); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); + } + } + }); + return this; + } + + public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out waiting for playback to end."); + } + release(); + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + return this; + } + + // Assertions called on the test thread after test finished. + + public void assertTimelinesEqual(Timeline... timelines) { + Assert.assertEquals(timelines.length, this.timelines.size()); + for (Timeline timeline : timelines) { + Assert.assertEquals(timeline, this.timelines.remove()); + } + } + + public void assertManifestsEqual(Object... manifests) { + Assert.assertEquals(manifests.length, this.manifests.size()); + for (Object manifest : manifests) { + Assert.assertEquals(manifest, this.manifests.remove()); + } + } + + public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { + Assert.assertEquals(trackGroupArray, this.trackGroups); + } + + public void assertPositionDiscontinuityCount(int expectedCount) { + Assert.assertEquals(expectedCount, positionDiscontinuityCount); + } + + public void assertPlayedPeriodIndices(int... periodIndices) { + Assert.assertEquals(periodIndices.length, this.periodIndices.size()); + for (int periodIndex : periodIndices) { + Assert.assertEquals(periodIndex, (int) this.periodIndices.remove()); + } + } + + // Private implementation details. + + private void release() throws InterruptedException { + handler.post(new Runnable() { + @Override + public void run() { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handleException(e); + } finally { + playerThread.quit(); + } + } + }); + playerThread.join(); + } + + private void handleException(Exception exception) { + if (this.exception == null) { + this.exception = exception; + } + endedCountDownLatch.countDown(); + } + + // Player.EventListener + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + timelines.add(timeline); + manifests.add(manifest); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + this.trackGroups = trackGroups; + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { + periodIndices.add(player.getCurrentPeriodIndex()); + } + if (playbackState == Player.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + handleException(exception); + } + + @Override + public void onPositionDiscontinuity() { + positionDiscontinuityCount++; + periodIndices.add(player.getCurrentPeriodIndex()); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java deleted file mode 100644 index ab247283e6..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.os.Handler; -import android.os.HandlerThread; -import android.util.Pair; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.LinkedList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import junit.framework.Assert; - -/** - * Wraps a player with its own handler thread. - */ -public class ExoPlayerWrapper implements Player.EventListener { - - private final CountDownLatch sourceInfoCountDownLatch; - private final CountDownLatch endedCountDownLatch; - private final HandlerThread playerThread; - private final Handler handler; - private final LinkedList> sourceInfos; - - public ExoPlayer player; - public TrackGroupArray trackGroups; - public Exception exception; - - // Written only on the main thread. - public volatile int positionDiscontinuityCount; - - public ExoPlayerWrapper() { - sourceInfoCountDownLatch = new CountDownLatch(1); - endedCountDownLatch = new CountDownLatch(1); - playerThread = new HandlerThread("ExoPlayerTest thread"); - playerThread.start(); - handler = new Handler(playerThread.getLooper()); - sourceInfos = new LinkedList<>(); - } - - // Called on the test thread. - - public void blockUntilEnded(long timeoutMs) throws Exception { - if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - exception = new TimeoutException("Test playback timed out waiting for playback to end."); - } - release(); - // Throw any pending exception (from playback, timing out or releasing). - if (exception != null) { - throw exception; - } - } - - public void blockUntilSourceInfoRefreshed(long timeoutMs) throws Exception { - if (!sourceInfoCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { - throw new TimeoutException("Test playback timed out waiting for source info."); - } - } - - public void setup(final MediaSource mediaSource, final Renderer... renderers) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); - player.addListener(ExoPlayerWrapper.this); - player.setPlayWhenReady(true); - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void prepare(final MediaSource mediaSource) { - handler.post(new Runnable() { - @Override - public void run() { - try { - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - public void release() throws InterruptedException { - handler.post(new Runnable() { - @Override - public void run() { - try { - if (player != null) { - player.release(); - } - } catch (Exception e) { - handleError(e); - } finally { - playerThread.quit(); - } - } - }); - playerThread.join(); - } - - private void handleError(Exception exception) { - if (this.exception == null) { - this.exception = exception; - } - endedCountDownLatch.countDown(); - } - - @SafeVarargs - public final void assertSourceInfosEquals(Pair... sourceInfos) { - Assert.assertEquals(sourceInfos.length, this.sourceInfos.size()); - for (Pair sourceInfo : sourceInfos) { - Assert.assertEquals(sourceInfo, this.sourceInfos.remove()); - } - } - - // Player.EventListener implementation. - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - endedCountDownLatch.countDown(); - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - sourceInfos.add(Pair.create(timeline, manifest)); - sourceInfoCountDownLatch.countDown(); - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - handleError(exception); - } - - @SuppressWarnings("NonAtomicVolatileUpdate") - @Override - public void onPositionDiscontinuity() { - positionDiscontinuityCount++; - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - -} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index f4476ddf93..add0c5d22f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.TrackGroup; /** * Fake data set emulating the data of an adaptive media source. - * It provides chunk data for all {@link Format}s in the given {@link TrackSelection}. + * It provides chunk data for all {@link Format}s in the given {@link TrackGroup}. */ public final class FakeAdaptiveDataSet extends FakeDataSet { @@ -36,8 +36,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { this.chunkDurationUs = chunkDurationUs; } - public FakeAdaptiveDataSet createDataSet(TrackSelection trackSelection, long mediaDurationUs) { - return new FakeAdaptiveDataSet(trackSelection, mediaDurationUs, chunkDurationUs); + public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) { + return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs); } } @@ -46,15 +46,14 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { private final long chunkDurationUs; private final long lastChunkDurationUs; - public FakeAdaptiveDataSet(TrackSelection trackSelection, long mediaDurationUs, - long chunkDurationUs) { + public FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs, long chunkDurationUs) { this.chunkDurationUs = chunkDurationUs; - int selectionCount = trackSelection.length(); + int trackCount = trackGroup.length; long lastChunkDurationUs = mediaDurationUs % chunkDurationUs; int fullChunks = (int) (mediaDurationUs / chunkDurationUs); - for (int i = 0; i < selectionCount; i++) { + for (int i = 0; i < trackCount; i++) { String uri = getUri(i); - Format format = trackSelection.getFormat(i); + Format format = trackGroup.getFormat(i); int chunkLength = (int) (format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND)); FakeData newData = this.newData(uri); for (int j = 0; j < fullChunks; j++) { @@ -74,8 +73,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { return chunkCount; } - public String getUri(int trackSelectionIndex) { - return "fake://adaptive.media/" + Integer.toString(trackSelectionIndex); + public String getUri(int trackIndex) { + return "fake://adaptive.media/" + trackIndex; } public long getChunkDuration(int chunkIndex) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java new file mode 100644 index 0000000000..c8757e69cd --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -0,0 +1,118 @@ +/* + * 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.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SequenceableLoader; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.List; + +/** + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a + * track will give the player a {@link ChunkSampleStream}. + */ +public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod + implements SequenceableLoader.Callback> { + + private final EventDispatcher eventDispatcher; + private final Allocator allocator; + private final FakeChunkSource.Factory chunkSourceFactory; + private final long durationUs; + + private Callback callback; + private ChunkSampleStream[] sampleStreams; + private SequenceableLoader sequenceableLoader; + + public FakeAdaptiveMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, + Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { + super(trackGroupArray); + this.eventDispatcher = eventDispatcher; + this.allocator = allocator; + this.chunkSourceFactory = chunkSourceFactory; + this.durationUs = durationUs; + } + + @Override + public void prepare(Callback callback, long positionUs) { + super.prepare(callback, positionUs); + this.callback = callback; + } + + @Override + @SuppressWarnings("unchecked") + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, + streamResetFlags, positionUs); + List> validStreams = new ArrayList<>(); + for (SampleStream stream : streams) { + if (stream != null) { + validStreams.add((ChunkSampleStream) stream); + } + } + this.sampleStreams = validStreams.toArray(new ChunkSampleStream[validStreams.size()]); + this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + return returnPositionUs; + } + + @Override + public long getBufferedPositionUs() { + super.getBufferedPositionUs(); + return sequenceableLoader.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.seekToUs(positionUs); + } + return super.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + super.getNextLoadPositionUs(); + return sequenceableLoader.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + super.continueLoading(positionUs); + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + protected SampleStream createSampleStream(TrackSelection trackSelection) { + FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); + return new ChunkSampleStream<>( + MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, + chunkSource, this, allocator, 0, 3, eventDispatcher); + } + + @Override + public void onContinueLoadingRequested(ChunkSampleStream source) { + callback.onContinueLoadingRequested(this); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java new file mode 100644 index 0000000000..59bcaf3e7c --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -0,0 +1,52 @@ +/* + * 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.os.Handler; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.Allocator; + +/** + * Fake {@link MediaSource} that provides a given timeline. Creating the period returns a + * {@link FakeAdaptiveMediaPeriod} from the given {@link TrackGroupArray}. + */ +public class FakeAdaptiveMediaSource extends FakeMediaSource { + + private final EventDispatcher eventDispatcher; + private final FakeChunkSource.Factory chunkSourceFactory; + + public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + super(timeline, manifest, trackGroupArray); + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.chunkSourceFactory = chunkSourceFactory; + } + + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + Period period = timeline.getPeriod(id.periodIndex, new Period()); + return new FakeAdaptiveMediaPeriod(trackGroupArray, eventDispatcher, allocator, + chunkSourceFactory, period.durationUs); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 0c970caa15..b8f25bfbce 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -50,7 +50,8 @@ public final class FakeChunkSource implements ChunkSource { } public FakeChunkSource createChunkSource(TrackSelection trackSelection, long durationUs) { - FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection, durationUs); + FakeAdaptiveDataSet dataSet = + dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs); dataSourceFactory.setFakeDataSet(dataSet); DataSource dataSource = dataSourceFactory.createDataSource(); return new FakeChunkSource(trackSelection, dataSource, dataSet); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 2580205361..fd85b02d78 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -162,8 +162,8 @@ public class FakeDataSet { } /** - * Appends data of the specified length. No actual data is available and this data should not - * be read. + * Appends a data segment of the specified length. No actual data is available and the + * {@link FakeDataSource} will perform no copy operations when this data is read. */ public FakeData appendReadData(int length) { Assertions.checkState(length > 0); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java index 6180a8aa77..aacd265e45 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSource.java @@ -166,6 +166,7 @@ public class FakeDataSource implements DataSource { // Do not allow crossing of the segment boundary. readLength = Math.min(readLength, current.length - current.bytesRead); // Perform the read and return. + Assertions.checkArgument(buffer.length - offset >= readLength); if (current.data != null) { System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d8e501a298..3863cf7987 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; @@ -26,10 +25,10 @@ import java.io.IOException; import junit.framework.Assert; /** - * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that - * track will give the player a {@link FakeSampleStream}. + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting + * tracks will give the player {@link FakeSampleStream}s. */ -public final class FakeMediaPeriod implements MediaPeriod { +public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; @@ -46,7 +45,6 @@ public final class FakeMediaPeriod implements MediaPeriod { @Override public void prepare(Callback callback, long positionUs) { Assert.assertFalse(preparedPeriod); - Assert.assertEquals(0, positionUs); preparedPeriod = true; callback.onPrepared(this); } @@ -71,8 +69,6 @@ public final class FakeMediaPeriod implements MediaPeriod { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { streams[i] = null; } - } - for (int i = 0; i < rendererCount; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; Assert.assertTrue(1 <= selection.length()); @@ -81,7 +77,7 @@ public final class FakeMediaPeriod implements MediaPeriod { int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex()); Assert.assertTrue(0 <= indexInTrackGroup); Assert.assertTrue(indexInTrackGroup < trackGroup.length); - streams[i] = new FakeSampleStream(selection.getSelectedFormat()); + streams[i] = createSampleStream(selection); streamResetFlags[i] = true; } } @@ -123,4 +119,8 @@ public final class FakeMediaPeriod implements MediaPeriod { return false; } + protected SampleStream createSampleStream(TrackSelection selection) { + return new FakeSampleStream(selection.getSelectedFormat()); + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index a2c1e9879e..9e7b498269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -34,7 +34,7 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - private final Timeline timeline; + protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; @@ -82,7 +82,7 @@ public class FakeMediaSource implements MediaSource { Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @@ -104,6 +104,11 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray); + } + private static TrackGroupArray buildTrackGroupArray(Format... formats) { TrackGroup[] trackGroups = new TrackGroup[formats.length]; for (int i = 0; i < formats.length; i++) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 4e1e32980f..699b850f73 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -60,8 +60,8 @@ public final class FakeSampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java new file mode 100644 index 0000000000..cf88d10bc8 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -0,0 +1,521 @@ +/* + * 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.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as + * possible without waiting. It does only support single period timelines and does not support + * updates during playback (like seek, timeline changes, repeat mode changes). + */ +public class FakeSimpleExoPlayer extends SimpleExoPlayer { + + private FakeExoPlayer player; + + public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, + LoadControl loadControl, FakeClock clock) { + super (renderersFactory, trackSelector, loadControl); + player.setFakeClock(clock); + } + + @Override + protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); + return player; + } + + private class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, MediaPeriod.Callback, + Runnable { + + private final Renderer[] renderers; + private final TrackSelector trackSelector; + private final LoadControl loadControl; + private final CopyOnWriteArraySet eventListeners; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Handler eventListenerHandler; + + private FakeClock clock; + private MediaSource mediaSource; + private Timeline timeline; + private Object manifest; + private MediaPeriod mediaPeriod; + private TrackSelectorResult selectorResult; + + private boolean isStartingUp; + private boolean isLoading; + private int playbackState; + private long rendererPositionUs; + private long durationUs; + private volatile long currentPositionMs; + private volatile long bufferedPositionMs; + + public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.eventListeners = new CopyOnWriteArraySet<>(); + Looper eventListenerLooper = Looper.myLooper(); + this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper + : Looper.getMainLooper()); + this.playbackThread = new HandlerThread("FakeExoPlayer Thread"); + playbackThread.start(); + this.playbackHandler = new Handler(playbackThread.getLooper()); + this.isStartingUp = true; + this.isLoading = false; + this.playbackState = Player.STATE_IDLE; + this.durationUs = C.TIME_UNSET; + } + + public void setFakeClock(FakeClock clock) { + this.clock = clock; + } + + @Override + public void addListener(Player.EventListener listener) { + eventListeners.add(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + eventListeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return playbackState; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (playWhenReady != true) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getPlayWhenReady() { + return true; + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + return Player.REPEAT_MODE_OFF; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + playbackThread.quit(); + } + + @Override + public int getRendererCount() { + return renderers.length; + } + + @Override + public int getRendererType(int index) { + return renderers[index].getTrackType(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return selectorResult != null ? selectorResult.groups : null; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return selectorResult != null ? selectorResult.selections : null; + } + + @Nullable + @Override + public Object getCurrentManifest() { + return manifest; + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public long getDuration() { + return C.usToMs(durationUs); + } + + @Override + public long getCurrentPosition() { + return currentPositionMs; + } + + @Override + public long getBufferedPosition() { + return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs; + } + + @Override + public int getBufferedPercentage() { + long duration = getDuration(); + return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration); + } + + @Override + public boolean isCurrentWindowDynamic() { + return false; + } + + @Override + public boolean isCurrentWindowSeekable() { + return false; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return 0; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return 0; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public Looper getPlaybackLooper() { + return playbackThread.getLooper(); + } + + @Override + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, true, true); + } + + @Override + public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (resetPosition != true || resetState != true) { + throw new UnsupportedOperationException(); + } + this.mediaSource = mediaSource; + playbackHandler.post(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this); + } + }); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + // MediaSource.Listener + + @Override + public void onSourceInfoRefreshed(final Timeline timeline, final @Nullable Object manifest) { + if (this.timeline != null) { + throw new UnsupportedOperationException(); + } + Assertions.checkArgument(timeline.getPeriodCount() == 1); + Assertions.checkArgument(timeline.getWindowCount() == 1); + final ConditionVariable waitForNotification = new ConditionVariable(); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; + FakeExoPlayer.this.timeline = timeline; + FakeExoPlayer.this.manifest = manifest; + eventListener.onTimelineChanged(timeline, manifest); + waitForNotification.open(); + } + } + }); + waitForNotification.block(); + this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator()); + mediaPeriod.prepare(this, 0); + } + + // MediaPeriod.Callback + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + maybeContinueLoading(); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + try { + initializePlaybackLoop(); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Runnable (Playback loop). + + @Override + public void run() { + try { + maybeContinueLoading(); + boolean allRenderersEnded = true; + boolean allRenderersReadyOrEnded = true; + if (playbackState == Player.STATE_READY) { + for (Renderer renderer : renderers) { + renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime())); + if (!renderer.isEnded()) { + allRenderersEnded = false; + } + if (!(renderer.isReady() || renderer.isEnded())) { + allRenderersReadyOrEnded = false; + } + } + } + if (rendererPositionUs >= durationUs && allRenderersEnded) { + changePlaybackState(Player.STATE_ENDED); + return; + } + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded + && haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) { + changePlaybackState(Player.STATE_READY); + isStartingUp = false; + } else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) { + changePlaybackState(Player.STATE_BUFFERING); + } + // Advance simulated time by 10ms. + clock.advanceTime(10); + if (playbackState == Player.STATE_READY) { + rendererPositionUs += 10000; + } + this.currentPositionMs = C.usToMs(rendererPositionUs); + this.bufferedPositionMs = C.usToMs(bufferedPositionUs); + playbackHandler.post(this); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Internal logic + + private void initializePlaybackLoop() throws ExoPlaybackException { + Assertions.checkNotNull(clock); + trackSelector.init(new InvalidationListener() { + @Override + public void onTrackSelectionsInvalidated() { + throw new IllegalStateException(); + } + }); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + selectorResult = trackSelector.selectTracks(rendererCapabilities, + mediaPeriod.getTrackGroups()); + SampleStream[] sampleStreams = new SampleStream[renderers.length]; + boolean[] mayRetainStreamFlags = new boolean[renderers.length]; + Arrays.fill(mayRetainStreamFlags, true); + mediaPeriod.selectTracks(selectorResult.selections.getAll(), mayRetainStreamFlags, + sampleStreams, new boolean[renderers.length], 0); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections); + } + } + }); + + loadControl.onPrepared(); + loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections); + + for (int i = 0; i < renderers.length; i++) { + TrackSelection selection = selectorResult.selections.get(i); + Format[] formats = new Format[selection.length()]; + for (int j = 0; j < formats.length; j++) { + formats[j] = selection.getFormat(j); + } + renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0, + false, 0); + renderers[i].setCurrentStreamFinal(); + } + + rendererPositionUs = 0; + changePlaybackState(Player.STATE_BUFFERING); + playbackHandler.post(this); + } + + private void maybeContinueLoading() { + boolean newIsLoading = false; + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { + long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; + if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + newIsLoading = true; + mediaPeriod.continueLoading(rendererPositionUs); + } + } + if (newIsLoading != isLoading) { + isLoading = newIsLoading; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onLoadingChanged(isLoading); + } + } + }); + } + } + + private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, + long bufferedPositionUs) { + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + return true; + } + return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + } + + private void handlePlayerError(final ExoPlaybackException e) { + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerError(e); + } + } + }); + changePlaybackState(Player.STATE_ENDED); + } + + private void changePlaybackState(final int playbackState) { + this.playbackState = playbackState; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerStateChanged(true, playbackState); + } + } + }); + } + + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 831344aa8b..8e4b9001dd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -24,7 +24,6 @@ import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.Bundle; import android.os.ConditionVariable; -import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; @@ -57,19 +56,20 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba void onStart(HostActivity host, Surface surface); /** - * Called on the main thread to check whether the test is ready to be stopped. + * Called on the main thread to block until the test has stopped or {@link #forceStop()} is + * called. * - * @return Whether the test is ready to be stopped. + * @param timeoutMs The maximum time to block in milliseconds. + * @return Whether the test has stopped successful. */ - boolean canStop(); + boolean blockUntilStopped(long timeoutMs); /** - * Called on the main thread when the test is stopped. - *

- * The test will be stopped if {@link #canStop()} returns true, if the {@link HostActivity} has - * been paused, or if the {@link HostActivity}'s {@link Surface} has been destroyed. + * Called on the main thread to force stop the test (if it is not stopped already). + * + * @return Whether the test was forced stopped. */ - void onStop(); + boolean forceStop(); /** * Called on the test thread after the test has finished and been stopped. @@ -85,13 +85,11 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba private WakeLock wakeLock; private WifiLock wifiLock; private SurfaceView surfaceView; - private Handler mainHandler; - private CheckCanStopRunnable checkCanStopRunnable; private HostedTest hostedTest; - private ConditionVariable hostedTestStoppedCondition; private boolean hostedTestStarted; - private boolean hostedTestFinished; + private ConditionVariable hostedTestStartedCondition; + private boolean forcedStopped; /** * Executes a {@link HostedTest} inside the host. @@ -100,7 +98,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout * is exceeded then the test will fail. */ - public void runTest(final HostedTest hostedTest, long timeoutMs) { + public void runTest(HostedTest hostedTest, long timeoutMs) { runTest(hostedTest, timeoutMs, true); } @@ -114,27 +112,28 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); - Assertions.checkState(this.hostedTest == null); - this.hostedTest = Assertions.checkNotNull(hostedTest); - hostedTestStoppedCondition = new ConditionVariable(); + Assertions.checkNotNull(hostedTest); + hostedTestStartedCondition = new ConditionVariable(); + forcedStopped = false; hostedTestStarted = false; - hostedTestFinished = false; runOnUiThread(new Runnable() { @Override public void run() { + HostActivity.this.hostedTest = hostedTest; maybeStartHostedTest(); } }); + hostedTestStartedCondition.block(); - if (hostedTestStoppedCondition.block(timeoutMs)) { - if (hostedTestFinished) { - Log.d(TAG, "Test finished. Checking pass conditions."); + if (hostedTest.blockUntilStopped(timeoutMs)) { + if (!forcedStopped) { + Log.d(TAG, "Checking test pass conditions."); hostedTest.onFinished(); Log.d(TAG, "Pass conditions checked."); } else { - String message = "Test released before it finished. Activity may have been paused whilst " + String message = "Test force stopped. Activity may have been paused whilst " + "test was in progress."; Log.e(TAG, message); fail(message); @@ -145,9 +144,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba if (failOnTimeout) { fail(message); } - maybeStopHostedTest(); - hostedTestStoppedCondition.block(); } + this.hostedTest = null; } // Activity lifecycle @@ -160,8 +158,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba surfaceView = (SurfaceView) findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); - mainHandler = new Handler(); - checkCanStopRunnable = new CheckCanStopRunnable(); } @Override @@ -176,12 +172,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba super.onStart(); } - @Override - public void onResume() { - super.onResume(); - maybeStartHostedTest(); - } - @Override public void onPause() { super.onPause(); @@ -225,24 +215,13 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba hostedTestStarted = true; Log.d(TAG, "Starting test."); hostedTest.onStart(this, surface); - checkCanStopRunnable.startChecking(); + hostedTestStartedCondition.open(); } } private void maybeStopHostedTest() { - if (hostedTest != null && hostedTestStarted) { - hostedTest.onStop(); - hostedTest = null; - mainHandler.removeCallbacks(checkCanStopRunnable); - // We post opening of the stopped condition so that any events posted to the main thread as a - // result of hostedTest.onStop() are guaranteed to be handled before hostedTest.onFinished() - // is called from runTest. - mainHandler.post(new Runnable() { - @Override - public void run() { - hostedTestStoppedCondition.open(); - } - }); + if (hostedTest != null && hostedTestStarted && !forcedStopped) { + forcedStopped = hostedTest.forceStop(); } } @@ -251,24 +230,4 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF; } - private final class CheckCanStopRunnable implements Runnable { - - private static final long CHECK_INTERVAL_MS = 1000; - - private void startChecking() { - mainHandler.post(this); - } - - @Override - public void run() { - if (hostedTest.canStop()) { - hostedTestFinished = true; - maybeStopHostedTest(); - } else { - mainHandler.postDelayed(this, CHECK_INTERVAL_MS); - } - } - - } - }