handlerPosts;
/**
* Create {@link FakeClock} with an arbitrary initial timestamp.
@@ -35,6 +37,7 @@ public final class FakeClock implements Clock {
public FakeClock(long initialTimeMs) {
this.currentTimeMs = initialTimeMs;
this.wakeUpTimes = new ArrayList<>();
+ this.handlerPosts = new ArrayList<>();
}
/**
@@ -50,10 +53,16 @@ public synchronized void advanceTime(long timeDiffMs) {
break;
}
}
+ for (int i = handlerPosts.size() - 1; i >= 0; i--) {
+ if (handlerPosts.get(i).postTime <= currentTimeMs) {
+ HandlerPostData postData = handlerPosts.remove(i);
+ postData.handler.post(postData.runnable);
+ }
+ }
}
@Override
- public long elapsedRealtime() {
+ public synchronized long elapsedRealtime() {
return currentTimeMs;
}
@@ -74,5 +83,28 @@ public synchronized void sleep(long sleepTimeMs) {
wakeUpTimes.remove(wakeUpTimeMs);
}
+ @Override
+ public synchronized void postDelayed(Handler handler, Runnable runnable, long delayMs) {
+ if (delayMs <= 0) {
+ handler.post(runnable);
+ } else {
+ handlerPosts.add(new HandlerPostData(currentTimeMs + delayMs, handler, runnable));
+ }
+ }
+
+ private static final class HandlerPostData {
+
+ public final long postTime;
+ public final Handler handler;
+ public final Runnable runnable;
+
+ public HandlerPostData(long postTime, Handler handler, Runnable runnable) {
+ this.postTime = postTime;
+ this.handler = handler;
+ this.runnable = runnable;
+ }
+
+ }
+
}
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 25802053614..e77e0714e7e 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
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.testutil;
+import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
@@ -28,11 +29,11 @@
/**
* Collection of {@link FakeData} to be served by a {@link FakeDataSource}.
*
- * Multiple fake data can be defined by {@link FakeDataSet#setData(String, byte[])} and {@link
- * FakeDataSet#newData(String)} methods. It's also possible to define a default data by {@link
+ *
Multiple fake data can be defined by {@link FakeDataSet#setData(Uri, byte[])} and {@link
+ * FakeDataSet#newData(Uri)} methods. It's also possible to define a default data by {@link
* FakeDataSet#newDefaultData()}.
*
- *
{@link FakeDataSet#newData(String)} and {@link FakeDataSet#newDefaultData()} return a {@link
+ *
{@link FakeDataSet#newData(Uri)} and {@link FakeDataSet#newDefaultData()} return a {@link
* FakeData} instance which can be used to define specific results during
* {@link FakeDataSource#read(byte[], int, int)} calls.
*
@@ -104,8 +105,8 @@ private Segment(Runnable action, Segment previousSegment) {
this(null, 0, null, action, previousSegment);
}
- private Segment(byte[] data, int length, IOException exception, Runnable action,
- Segment previousSegment) {
+ private Segment(@Nullable byte[] data, int length, @Nullable IOException exception,
+ @Nullable Runnable action, Segment previousSegment) {
this.exception = exception;
this.action = action;
this.data = data;
@@ -125,12 +126,12 @@ public boolean isActionSegment() {
}
/** Uri of the data or null if this is the default FakeData. */
- public final String uri;
+ public final Uri uri;
private final ArrayList segments;
private final FakeDataSet dataSet;
private boolean simulateUnknownLength;
- private FakeData(FakeDataSet dataSet, String uri) {
+ private FakeData(FakeDataSet dataSet, Uri uri) {
this.uri = uri;
this.segments = new ArrayList<>();
this.dataSet = dataSet;
@@ -162,8 +163,8 @@ public FakeData appendReadData(byte[] data) {
}
/**
- * 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);
@@ -219,7 +220,7 @@ private Segment getLastSegment() {
}
- private final HashMap dataMap;
+ private final HashMap dataMap;
private FakeData defaultData;
public FakeDataSet() {
@@ -234,16 +235,31 @@ public FakeData newDefaultData() {
/** Sets random data with the given {@code length} for the given {@code uri}. */
public FakeDataSet setRandomData(String uri, int length) {
+ return setRandomData(Uri.parse(uri), length);
+ }
+
+ /** Sets random data with the given {@code length} for the given {@code uri}. */
+ public FakeDataSet setRandomData(Uri uri, int length) {
return setData(uri, TestUtil.buildTestData(length));
}
/** Sets the given {@code data} for the given {@code uri}. */
public FakeDataSet setData(String uri, byte[] data) {
+ return setData(Uri.parse(uri), data);
+ }
+
+ /** Sets the given {@code data} for the given {@code uri}. */
+ public FakeDataSet setData(Uri uri, byte[] data) {
return newData(uri).appendReadData(data).endData();
}
/** Returns a new {@link FakeData} with the given {@code uri}. */
public FakeData newData(String uri) {
+ return newData(Uri.parse(uri));
+ }
+
+ /** Returns a new {@link FakeData} with the given {@code uri}. */
+ public FakeData newData(Uri uri) {
FakeData data = new FakeData(this, uri);
dataMap.put(uri, data);
return data;
@@ -251,6 +267,11 @@ public FakeData newData(String uri) {
/** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */
public FakeData getData(String uri) {
+ return getData(Uri.parse(uri));
+ }
+
+ /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */
+ public FakeData getData(Uri uri) {
FakeData data = dataMap.get(uri);
return data != null ? data : defaultData;
}
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 6180a8aa77a..2675e1f0d7d 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 final int read(byte[] buffer, int offset, int readLength) throws IOExcept
// 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);
}
@@ -216,7 +217,7 @@ public final DataSpec[] getAndClearOpenedDataSpecs() {
return dataSpecs;
}
- protected void onDataRead(int bytesRead) {
+ protected void onDataRead(int bytesRead) throws IOException {
// Do nothing. Can be overridden.
}
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 d8e501a2984..38a5e37fa53 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 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 void release() {
@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 long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF
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,11 +77,11 @@ public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamF
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;
}
}
- return 0;
+ return positionUs;
}
@Override
@@ -123,4 +119,8 @@ public boolean continueLoading(long positionUs) {
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 93134bf3122..1f2524110a9 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,10 +34,11 @@
*/
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;
+ private final ArrayList createdMediaPeriods;
private boolean preparedSource;
private boolean releasedSource;
@@ -58,18 +59,15 @@ public FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray track
this.timeline = timeline;
this.manifest = manifest;
this.activeMediaPeriods = new ArrayList<>();
+ this.createdMediaPeriods = new ArrayList<>();
this.trackGroupArray = trackGroupArray;
}
- public void assertReleased() {
- Assert.assertTrue(releasedSource);
- }
-
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Assert.assertFalse(preparedSource);
preparedSource = true;
- listener.onSourceInfoRefreshed(timeline, manifest);
+ listener.onSourceInfoRefreshed(this, timeline, manifest);
}
@Override
@@ -82,8 +80,9 @@ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
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);
+ createdMediaPeriods.add(id);
return mediaPeriod;
}
@@ -104,6 +103,25 @@ public void releaseSource() {
releasedSource = true;
}
+ /**
+ * Assert that the source and all periods have been released.
+ */
+ public void assertReleased() {
+ Assert.assertTrue(releasedSource);
+ }
+
+ /**
+ * Assert that a media period for the given id has been created.
+ */
+ public void assertMediaPeriodCreated(MediaPeriodId mediaPeriodId) {
+ Assert.assertTrue(createdMediaPeriods.contains(mediaPeriodId));
+ }
+
+ 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/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java
index a66043b77ff..c4270eb9c47 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java
@@ -59,6 +59,7 @@ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlayb
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (!isEnded) {
+ buffer.clear();
// Verify the format matches the expected format.
FormatHolder formatHolder = new FormatHolder();
int result = readSource(formatHolder, buffer, false);
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 4e1e32980fe..699b850f73f 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 void maybeThrowError() throws IOException {
}
@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/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java
new file mode 100644
index 00000000000..0664f470231
--- /dev/null
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.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.testutil;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.source.ShuffleOrder;
+
+/**
+ * Fake {@link ShuffleOrder} which returns a reverse order. This order is thus deterministic but
+ * different from the original order.
+ */
+public final class FakeShuffleOrder implements ShuffleOrder {
+
+ private final int length;
+
+ public FakeShuffleOrder(int length) {
+ this.length = length;
+ }
+
+ @Override
+ public int getLength() {
+ return length;
+ }
+
+ @Override
+ public int getNextIndex(int index) {
+ return index > 0 ? index - 1 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousIndex(int index) {
+ return index < length - 1 ? index + 1 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getLastIndex() {
+ return length > 0 ? 0 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getFirstIndex() {
+ return length > 0 ? length - 1 : C.INDEX_UNSET;
+ }
+
+ @Override
+ public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
+ return new FakeShuffleOrder(length + insertionCount);
+ }
+
+ @Override
+ public ShuffleOrder cloneAndRemove(int removalIndex) {
+ return new FakeShuffleOrder(length - 1);
+ }
+
+}
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 00000000000..4d53a6c89de
--- /dev/null
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java
@@ -0,0 +1,575 @@
+/*
+ * 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 static 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) {
+ 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 void setShuffleModeEnabled(boolean shuffleModeEnabled) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean getShuffleModeEnabled() {
+ return false;
+ }
+
+ @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() {
+ playbackHandler.post(new Runnable() {
+ @Override
+ public void run () {
+ playbackHandler.removeCallbacksAndMessages(null);
+ releaseMedia();
+ changePlaybackState(Player.STATE_IDLE);
+ }
+ });
+ }
+
+ @Override
+ @SuppressWarnings("ThreadJoinLoop")
+ public void release() {
+ stop();
+ playbackHandler.post(new Runnable() {
+ @Override
+ public void run () {
+ playbackHandler.removeCallbacksAndMessages(null);
+ playbackThread.quit();
+ }
+ });
+ while (playbackThread.isAlive()) {
+ try {
+ playbackThread.join();
+ } catch (InterruptedException e) {
+ // Ignore interrupt.
+ }
+ }
+ }
+
+ @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 int getNextWindowIndex() {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getPreviousWindowIndex() {
+ return C.INDEX_UNSET;
+ }
+
+ @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 || !resetState) {
+ 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(MediaSource source, 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);
+ }
+ }
+ });
+ }
+
+ private void releaseMedia() {
+ if (mediaSource != null) {
+ if (mediaPeriod != null) {
+ mediaSource.releasePeriod(mediaPeriod);
+ mediaPeriod = null;
+ }
+ mediaSource.releaseSource();
+ mediaSource = null;
+ }
+ }
+
+ }
+
+}
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
index 040782264b4..2937ee27708 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java
@@ -18,6 +18,7 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.Util;
+import java.util.Arrays;
/**
* Fake {@link Timeline} which can be setup to return custom {@link TimelineWindowDefinition}s.
@@ -36,6 +37,8 @@ public static final class TimelineWindowDefinition {
public final boolean isSeekable;
public final boolean isDynamic;
public final long durationUs;
+ public final int adGroupsPerPeriodCount;
+ public final int adsPerAdGroupCount;
public TimelineWindowDefinition(int periodCount, Object id) {
this(periodCount, id, true, false, WINDOW_DURATION_US);
@@ -47,15 +50,24 @@ public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long dura
public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable,
boolean isDynamic, long durationUs) {
+ this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0);
+ }
+
+ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable,
+ boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) {
this.periodCount = periodCount;
this.id = id;
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.durationUs = durationUs;
+ this.adGroupsPerPeriodCount = adGroupsCountPerPeriod;
+ this.adsPerAdGroupCount = adsPerAdGroupCount;
}
}
+ private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND;
+
private final TimelineWindowDefinition[] windowDefinitions;
private final int[] periodOffsets;
@@ -96,7 +108,28 @@ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
Object id = setIds ? windowPeriodIndex : null;
Object uid = setIds ? periodIndex : null;
long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount;
- return period.set(id, uid, windowIndex, periodDurationUs, periodDurationUs * windowPeriodIndex);
+ long positionInWindowUs = periodDurationUs * windowPeriodIndex;
+ if (windowDefinition.adGroupsPerPeriodCount == 0) {
+ return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs);
+ } else {
+ int adGroups = windowDefinition.adGroupsPerPeriodCount;
+ long[] adGroupTimesUs = new long[adGroups];
+ int[] adCounts = new int[adGroups];
+ int[] adLoadedAndPlayedCounts = new int[adGroups];
+ long[][] adDurationsUs = new long[adGroups][];
+ long adResumePositionUs = 0;
+ long adGroupOffset = adGroups > 1 ? periodDurationUs / (adGroups - 1) : 0;
+ for (int i = 0; i < adGroups; i++) {
+ adGroupTimesUs[i] = i * adGroupOffset;
+ adCounts[i] = windowDefinition.adsPerAdGroupCount;
+ adLoadedAndPlayedCounts[i] = 0;
+ adDurationsUs[i] = new long[adCounts[i]];
+ Arrays.fill(adDurationsUs[i], AD_DURATION_US);
+ }
+ return period.set(id, uid, windowIndex, periodDurationUs, positionInWindowUs, adGroupTimesUs,
+ adCounts, adLoadedAndPlayedCounts, adLoadedAndPlayedCounts, adDurationsUs,
+ adResumePositionUs);
+ }
}
@Override
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 831344aa8be..1ef1acd80bf 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
@@ -17,14 +17,12 @@
import static junit.framework.Assert.fail;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
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 +55,20 @@ public interface HostedTest {
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 +84,11 @@ public interface HostedTest {
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 +97,7 @@ public interface HostedTest {
* @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,40 +111,46 @@ public void runTest(final HostedTest hostedTest, long timeoutMs) {
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);
}
} else {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ hostedTest.forceStop();
+ }
+ });
String message = "Test timed out after " + timeoutMs + " ms.";
Log.e(TAG, message);
if (failOnTimeout) {
fail(message);
}
- maybeStopHostedTest();
- hostedTestStoppedCondition.block();
}
+ this.hostedTest = null;
}
// Activity lifecycle
@@ -157,18 +160,16 @@ public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName()));
- surfaceView = (SurfaceView) findViewById(
+ surfaceView = findViewById(
getResources().getIdentifier("surface_view", "id", getPackageName()));
surfaceView.getHolder().addCallback(this);
- mainHandler = new Handler();
- checkCanStopRunnable = new CheckCanStopRunnable();
}
@Override
public void onStart() {
Context appContext = getApplicationContext();
WifiManager wifiManager = (WifiManager) appContext.getSystemService(Context.WIFI_SERVICE);
- wifiLock = wifiManager.createWifiLock(getWifiLockMode(), TAG);
+ wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, TAG);
wifiLock.acquire();
PowerManager powerManager = (PowerManager) appContext.getSystemService(Context.POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
@@ -176,21 +177,20 @@ public void onStart() {
super.onStart();
}
- @Override
- public void onResume() {
- super.onResume();
- maybeStartHostedTest();
- }
-
@Override
public void onPause() {
super.onPause();
- maybeStopHostedTest();
+ if (Util.SDK_INT <= 23) {
+ maybeStopHostedTest();
+ }
}
@Override
public void onStop() {
super.onStop();
+ if (Util.SDK_INT > 23) {
+ maybeStopHostedTest();
+ }
wakeLock.release();
wakeLock = null;
wifiLock.release();
@@ -225,50 +225,14 @@ private void maybeStartHostedTest() {
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();
- }
- });
- }
- }
-
- @SuppressLint("InlinedApi")
- private static int getWifiLockMode() {
- 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);
- }
+ if (hostedTest != null && hostedTestStarted && !forcedStopped) {
+ forcedStopped = hostedTest.forceStop();
}
-
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java
similarity index 99%
rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java
rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java
index cef033bf17e..88b5de7f65d 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java
@@ -13,19 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.android.exoplayer2.extractor.ogg;
+package com.google.android.exoplayer2.testutil;
-import com.google.android.exoplayer2.testutil.FakeExtractorInput;
-import com.google.android.exoplayer2.testutil.TestUtil;
/**
* Provides ogg/vorbis test data in bytes for unit tests.
*/
-/* package */ final class TestData {
+public final class OggTestData {
- /* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) {
+ public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) {
return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true)
- .setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build();
+ .setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build();
}
public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter,
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java
index 2e59b33c0b6..61d1ecaeea7 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java
@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.testutil;
import android.app.Instrumentation;
-import android.test.InstrumentationTestCase;
+import android.content.Context;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
@@ -33,7 +33,6 @@
import java.util.Arrays;
import java.util.Random;
import junit.framework.Assert;
-import org.mockito.MockitoAnnotations;
/**
* Utility methods for tests.
@@ -121,21 +120,22 @@ public static byte[] joinByteArrays(byte[]... byteArrays) {
return joined;
}
- public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) {
- // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2.
- System.setProperty("dexmaker.dexcache",
- instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath());
- MockitoAnnotations.initMocks(instrumentationTestCase);
- }
-
public static byte[] getByteArray(Instrumentation instrumentation, String fileName)
throws IOException {
- return Util.toByteArray(getInputStream(instrumentation, fileName));
+ return getByteArray(instrumentation.getContext(), fileName);
+ }
+
+ public static byte[] getByteArray(Context context, String fileName) throws IOException {
+ return Util.toByteArray(getInputStream(context, fileName));
}
public static InputStream getInputStream(Instrumentation instrumentation, String fileName)
throws IOException {
- return instrumentation.getContext().getResources().getAssets().open(fileName);
+ return getInputStream(instrumentation.getContext(), fileName);
+ }
+
+ public static InputStream getInputStream(Context context, String fileName) throws IOException {
+ return context.getResources().getAssets().open(fileName);
}
public static String getString(Instrumentation instrumentation, String fileName)
@@ -150,7 +150,8 @@ public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) {
class TimelineListener implements Listener {
private Timeline timeline;
@Override
- public synchronized void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ public synchronized void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
+ Object manifest) {
this.timeline = timeline;
this.notify();
}
@@ -175,13 +176,15 @@ public synchronized void onSourceInfoRefreshed(Timeline timeline, Object manifes
* @param dataSource The {@link DataSource} through which to read.
* @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}.
* @param expectedData The expected data.
+ * @param expectKnownLength Whether to assert that {@link DataSource#open} returns the expected
+ * data length. If false then it's asserted that {@link C#LENGTH_UNSET} is returned.
* @throws IOException If an error occurs reading fom the {@link DataSource}.
*/
public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec,
- byte[] expectedData) throws IOException {
+ byte[] expectedData, boolean expectKnownLength) throws IOException {
try {
long length = dataSource.open(dataSpec);
- Assert.assertEquals(expectedData.length, length);
+ Assert.assertEquals(expectKnownLength ? expectedData.length : C.LENGTH_UNSET, length);
byte[] readData = TestUtil.readToEnd(dataSource);
MoreAsserts.assertEquals(expectedData, readData);
} finally {
diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
index 8357ce70c7f..b1df8f62e15 100644
--- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
+++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java
@@ -16,12 +16,19 @@
package com.google.android.exoplayer2.testutil;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import android.os.ConditionVariable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaPeriod.Callback;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
/**
* Unit test for {@link Timeline}.
@@ -36,6 +43,10 @@ private TimelineAsserts() {}
public static void assertEmpty(Timeline timeline) {
assertWindowIds(timeline);
assertPeriodCounts(timeline);
+ for (boolean shuffled : new boolean[] {false, true}) {
+ assertEquals(C.INDEX_UNSET, timeline.getFirstWindowIndex(shuffled));
+ assertEquals(C.INDEX_UNSET, timeline.getLastWindowIndex(shuffled));
+ }
}
/**
@@ -56,7 +67,7 @@ public static void assertWindowIds(Timeline timeline, Object... expectedWindowId
}
/**
- * Asserts that window properties {@link Window}.isDynamic are set correctly..
+ * Asserts that window properties {@link Window}.isDynamic are set correctly.
*/
public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsDynamic) {
Window window = new Window();
@@ -67,33 +78,34 @@ public static void assertWindowIsDynamic(Timeline timeline, boolean... windowIsD
}
/**
- * Asserts that previous window indices for each window are set correctly depending on the repeat
- * mode.
+ * Asserts that previous window indices for each window depending on the repeat mode and the
+ * shuffle mode are equal to the given sequence.
*/
public static void assertPreviousWindowIndices(Timeline timeline,
- @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) {
+ @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled,
+ int... expectedPreviousWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) {
assertEquals(expectedPreviousWindowIndices[i],
- timeline.getPreviousWindowIndex(i, repeatMode));
+ timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled));
}
}
/**
- * Asserts that next window indices for each window are set correctly depending on the repeat
- * mode.
+ * Asserts that next window indices for each window depending on the repeat mode and the
+ * shuffle mode are equal to the given sequence.
*/
public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode,
- int... expectedNextWindowIndices) {
+ boolean shuffleModeEnabled, int... expectedNextWindowIndices) {
for (int i = 0; i < timeline.getWindowCount(); i++) {
assertEquals(expectedNextWindowIndices[i],
- timeline.getNextWindowIndex(i, repeatMode));
+ timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled));
}
}
/**
* Asserts that period counts for each window are set correctly. Also asserts that
* {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it
- * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int)}.
+ * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}.
*/
public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) {
int windowCount = timeline.getWindowCount();
@@ -118,31 +130,73 @@ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCo
expectedWindowIndex++;
}
assertEquals(expectedWindowIndex, period.windowIndex);
- if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {
- assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF));
- assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE));
- assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL));
- } else {
- int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex,
- Player.REPEAT_MODE_OFF);
- int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex,
- Player.REPEAT_MODE_ONE);
- int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex,
- Player.REPEAT_MODE_ALL);
- int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET
- : accumulatedPeriodCounts[nextWindowOff];
- int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET
- : accumulatedPeriodCounts[nextWindowOne];
- int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET
- : accumulatedPeriodCounts[nextWindowAll];
- assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window,
- Player.REPEAT_MODE_OFF));
- assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window,
- Player.REPEAT_MODE_ONE));
- assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window,
- Player.REPEAT_MODE_ALL));
+ assertEquals(i, timeline.getIndexOfPeriod(period.uid));
+ for (@Player.RepeatMode int repeatMode
+ : new int[] {Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL}) {
+ if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) {
+ assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false));
+ } else {
+ int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false);
+ int nextPeriod = nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET
+ : accumulatedPeriodCounts[nextWindow];
+ assertEquals(nextPeriod, timeline.getNextPeriodIndex(i, period, window, repeatMode,
+ false));
+ }
+ }
+ }
+ }
+
+ /**
+ * Asserts that periods' {@link Period#getAdGroupCount()} are set correctly.
+ */
+ public static void assertAdGroupCounts(Timeline timeline, int... expectedAdGroupCounts) {
+ Period period = new Period();
+ for (int i = 0; i < timeline.getPeriodCount(); i++) {
+ timeline.getPeriod(i, period);
+ assertEquals(expectedAdGroupCounts[i], period.getAdGroupCount());
+ }
+ }
+
+ /**
+ * Asserts that all period (including ad periods) can be created from the source, prepared, and
+ * released without exception and within timeout.
+ */
+ public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource,
+ Timeline timeline, long timeoutMs) {
+ Period period = new Period();
+ for (int i = 0; i < timeline.getPeriodCount(); i++) {
+ assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs);
+ timeline.getPeriod(i, period);
+ for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) {
+ for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) {
+ assertPeriodCanBeCreatedPreparedAndReleased(mediaSource,
+ new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs);
+ }
}
}
}
+ private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource,
+ MediaPeriodId mediaPeriodId, long timeoutMs) {
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, null);
+ assertNotNull(mediaPeriod);
+ final ConditionVariable mediaPeriodPrepared = new ConditionVariable();
+ mediaPeriod.prepare(new Callback() {
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ mediaPeriodPrepared.open();
+ }
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {}
+ }, /* positionUs= */ 0);
+ assertTrue(mediaPeriodPrepared.block(timeoutMs));
+ // MediaSource is supposed to support multiple calls to createPeriod with the same id without an
+ // intervening call to releasePeriod.
+ MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null);
+ assertNotNull(secondMediaPeriod);
+ mediaSource.releasePeriod(secondMediaPeriod);
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+
}
+