Commit 0425ff6d by Oliver Woodman

Merge branch 'dev-v2' into release-v2

parents deb9b301 427411be
Showing with 2827 additions and 659 deletions
......@@ -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 {
......
......@@ -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;
......
......@@ -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;
......
......@@ -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) {
......
......@@ -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;
......@@ -64,33 +64,12 @@ import com.google.android.exoplayer2.util.Assertions;
}
@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);
}
}
......@@ -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;
......
......@@ -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<Integer> 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));
}
}
......@@ -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;
......
......@@ -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();
......
......@@ -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);
}
......
......@@ -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);
}
......
/*
* 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);
}
}
}
......@@ -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;
......
......@@ -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++) {
......
......@@ -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());
......
......@@ -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);
}
/**
......
......@@ -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) {
......
......@@ -37,6 +37,12 @@ public final class DecoderCounters {
*/
public int inputBufferCount;
/**
* The number of skipped input buffers.
* <p>
* 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.
*/
public int renderedOutputBufferCount;
......@@ -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;
......
......@@ -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;
......
......@@ -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);
......
......@@ -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;
......
......@@ -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) {
......
......@@ -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));
......
......@@ -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.
......
/*
* 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<Integer, Integer> 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);
}
}
......@@ -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:
......
......@@ -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();
......
......@@ -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;
......
......@@ -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;
}
......@@ -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
......
......@@ -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);
}
}
......
......@@ -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,27 +154,11 @@ 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) {
window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs);
......@@ -197,22 +180,12 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
}
@Override
public int getPeriodCount() {
return 1;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
period = timeline.getPeriod(0, period, setIds);
period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
return period;
}
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(uid);
}
}
}
......@@ -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();
}
}
......
......@@ -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;
}
}
......@@ -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);
}
}
......
/*
* 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);
}
}
......@@ -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);
}
}
}
......@@ -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;
}
/**
......
......@@ -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);
}
......
......@@ -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);
}
/*
* 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);
}
......@@ -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;
}
}
......
......@@ -160,6 +160,7 @@ public class ChunkSampleStream<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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<T extends ChunkSource> 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;
}
}
......
......@@ -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};
......
......@@ -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<String> cachedContentToBeRemoved = new LinkedList<>();
ArrayList<String> 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));
}
}
......
......@@ -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<CacheSpan> spansToBeRemoved = new LinkedList<>();
ArrayList<CacheSpan> 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();
......
......@@ -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;
......
......@@ -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<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig) {
ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig);
public static Pair<Integer, Integer> 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<Integer, Integer> 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.
}
}
}
......@@ -175,6 +175,43 @@ public final class ParsableBitArray {
}
/**
* 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.
*/
public void byteAlign() {
......
......@@ -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}));
}
}
......@@ -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
......
......@@ -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);
}
}
......@@ -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
......
......@@ -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;
......@@ -109,6 +110,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
}
/**
* 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<String> renditionUrls) {
return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls),
copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls),
muxedAudioFormat, muxedCaptionFormats);
}
/**
* Creates a playlist with a single variant.
*
* @param variantUrl The url of the single variant.
......@@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
emptyList, null, null);
}
private static List<HlsUrl> copyRenditionsList(List<HlsUrl> renditions, List<String> urls) {
List<HlsUrl> 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;
}
}
......@@ -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;
}
......
<!-- 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20
17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04
2.04-3.13-3.13z" />
</vector>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Herhaal alles"</string>
<string name="exo_controls_repeat_off_description">"Herhaal niks"</string>
<string name="exo_controls_repeat_one_description">"Herhaal een"</string>
<string name="exo_controls_shuffle_description">"Skommel"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ሁሉንም ድገም"</string>
<string name="exo_controls_repeat_off_description">"ምንም አትድገም"</string>
<string name="exo_controls_repeat_one_description">"አንዱን ድገም"</string>
<string name="exo_controls_shuffle_description">"በው"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"تكرار الكل"</string>
<string name="exo_controls_repeat_off_description">"عدم التكرار"</string>
<string name="exo_controls_repeat_one_description">"تكرار مقطع واحد"</string>
<string name="exo_controls_shuffle_description">"ترتيب عشوائي"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Bütün təkrarlayın"</string>
<string name="exo_controls_repeat_one_description">"Təkrar bir"</string>
<string name="exo_controls_repeat_off_description">"Heç bir təkrar"</string>
<string name="exo_controls_shuffle_description">"Qarışdır"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovi sve"</string>
<string name="exo_controls_repeat_off_description">"Ne ponavljaj nijednu"</string>
<string name="exo_controls_repeat_one_description">"Ponovi jednu"</string>
<string name="exo_controls_shuffle_description">"Pusti nasumično"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Паўтарыць усё"</string>
<string name="exo_controls_repeat_off_description">"Паўтараць ні"</string>
<string name="exo_controls_repeat_one_description">"Паўтарыць адзін"</string>
<string name="exo_controls_shuffle_description">"Перамяшаць"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Повтаряне на всички"</string>
<string name="exo_controls_repeat_off_description">"Без повтаряне"</string>
<string name="exo_controls_repeat_one_description">"Повтаряне на един елемент"</string>
<string name="exo_controls_shuffle_description">"Разбъркване"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string>
<string name="exo_controls_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string>
<string name="exo_controls_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string>
<string name="exo_controls_shuffle_description">"অদলবদল"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovite sve"</string>
<string name="exo_controls_repeat_off_description">"Ne ponavljaju"</string>
<string name="exo_controls_repeat_one_description">"Ponovite jedan"</string>
<string name="exo_controls_shuffle_description">"Izmiješaj"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeteix-ho tot"</string>
<string name="exo_controls_repeat_off_description">"No en repeteixis cap"</string>
<string name="exo_controls_repeat_one_description">"Repeteix-ne un"</string>
<string name="exo_controls_shuffle_description">"Reprodueix aleatòriament"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Opakovat vše"</string>
<string name="exo_controls_repeat_off_description">"Neopakovat"</string>
<string name="exo_controls_repeat_one_description">"Opakovat jednu položku"</string>
<string name="exo_controls_shuffle_description">"Náhodně"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Gentag alle"</string>
<string name="exo_controls_repeat_off_description">"Gentag ingen"</string>
<string name="exo_controls_repeat_one_description">"Gentag en"</string>
<string name="exo_controls_shuffle_description">"Bland"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Alle wiederholen"</string>
<string name="exo_controls_repeat_off_description">"Keinen Titel wiederholen"</string>
<string name="exo_controls_repeat_one_description">"Einen Titel wiederholen"</string>
<string name="exo_controls_shuffle_description">"Zufallsmix"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Επανάληψη όλων"</string>
<string name="exo_controls_repeat_off_description">"Καμία επανάληψη"</string>
<string name="exo_controls_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
<string name="exo_controls_shuffle_description">"Τυχαία αναπαραγωγή"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir todo"</string>
<string name="exo_controls_repeat_off_description">"No repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir uno"</string>
<string name="exo_controls_shuffle_description">"Reproducir aleatoriamente"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir todo"</string>
<string name="exo_controls_repeat_off_description">"No repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir uno"</string>
<string name="exo_controls_shuffle_description">"Reproducción aleatoria"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Korda kõike"</string>
<string name="exo_controls_repeat_off_description">"Ära korda midagi"</string>
<string name="exo_controls_repeat_one_description">"Korda ühte"</string>
<string name="exo_controls_shuffle_description">"Esita juhuslikus järjekorras"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Errepikatu guztiak"</string>
<string name="exo_controls_repeat_off_description">"Ez errepikatu"</string>
<string name="exo_controls_repeat_one_description">"Errepikatu bat"</string>
<string name="exo_controls_shuffle_description">"Erreproduzitu ausaz"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"تکرار همه"</string>
<string name="exo_controls_repeat_off_description">"تکرار هیچ‌کدام"</string>
<string name="exo_controls_repeat_one_description">"یک‌بار تکرار"</string>
<string name="exo_controls_shuffle_description">"پخش درهم"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Toista kaikki"</string>
<string name="exo_controls_repeat_off_description">"Toista ei mitään"</string>
<string name="exo_controls_repeat_one_description">"Toista yksi"</string>
<string name="exo_controls_shuffle_description">"Toista satunnaisesti"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_controls_repeat_off_description">"Aucune répétition"</string>
<string name="exo_controls_repeat_one_description">"Répéter un élément"</string>
<string name="exo_controls_shuffle_description">"Lecture aléatoire"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Tout lire en boucle"</string>
<string name="exo_controls_repeat_off_description">"Ne rien lire en boucle"</string>
<string name="exo_controls_repeat_one_description">"Lire en boucle un élément"</string>
<string name="exo_controls_shuffle_description">"Lire en mode aléatoire"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir todo"</string>
<string name="exo_controls_repeat_off_description">"Non repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir un"</string>
<string name="exo_controls_shuffle_description">"Reprodución aleatoria"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"બધા પુનરાવર્તન કરો"</string>
<string name="exo_controls_repeat_off_description">"કંઈ પુનરાવર્તન કરો"</string>
<string name="exo_controls_repeat_one_description">"એક પુનરાવર્તન કરો"</string>
<string name="exo_controls_shuffle_description">"શફલ કરો"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"सभी को दोहराएं"</string>
<string name="exo_controls_repeat_off_description">"कुछ भी न दोहराएं"</string>
<string name="exo_controls_repeat_one_description">"एक दोहराएं"</string>
<string name="exo_controls_shuffle_description">"शफ़ल करें"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovi sve"</string>
<string name="exo_controls_repeat_off_description">"Bez ponavljanja"</string>
<string name="exo_controls_repeat_one_description">"Ponovi jedno"</string>
<string name="exo_controls_shuffle_description">"Reproduciraj nasumično"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Összes ismétlése"</string>
<string name="exo_controls_repeat_off_description">"Nincs ismétlés"</string>
<string name="exo_controls_repeat_one_description">"Egy ismétlése"</string>
<string name="exo_controls_shuffle_description">"Véletlenszerű lejátszás"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"կրկնել այն ամենը"</string>
<string name="exo_controls_repeat_off_description">"Չկրկնել"</string>
<string name="exo_controls_repeat_one_description">"Կրկնել մեկը"</string>
<string name="exo_controls_shuffle_description">"Խառնել"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ulangi Semua"</string>
<string name="exo_controls_repeat_off_description">"Jangan Ulangi"</string>
<string name="exo_controls_repeat_one_description">"Ulangi Satu"</string>
<string name="exo_controls_shuffle_description">"Acak"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Endurtaka allt"</string>
<string name="exo_controls_repeat_off_description">"Endurtaka ekkert"</string>
<string name="exo_controls_repeat_one_description">"Endurtaka eitt"</string>
<string name="exo_controls_shuffle_description">"Stokka"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ripeti tutti"</string>
<string name="exo_controls_repeat_off_description">"Non ripetere nessuno"</string>
<string name="exo_controls_repeat_one_description">"Ripeti uno"</string>
<string name="exo_controls_shuffle_description">"Riproduci casualmente"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"חזור על הכל"</string>
<string name="exo_controls_repeat_off_description">"אל תחזור על כלום"</string>
<string name="exo_controls_repeat_one_description">"חזור על פריט אחד"</string>
<string name="exo_controls_shuffle_description">"ערבב"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"全曲を繰り返し"</string>
<string name="exo_controls_repeat_off_description">"繰り返しなし"</string>
<string name="exo_controls_repeat_one_description">"1曲を繰り返し"</string>
<string name="exo_controls_shuffle_description">"シャッフル"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"გამეორება ყველა"</string>
<string name="exo_controls_repeat_off_description">"გაიმეორეთ არცერთი"</string>
<string name="exo_controls_repeat_one_description">"გაიმეორეთ ერთი"</string>
<string name="exo_controls_shuffle_description">"არეულად დაკვრა"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Барлығын қайталау"</string>
<string name="exo_controls_repeat_off_description">"Ешқайсысын қайталамау"</string>
<string name="exo_controls_repeat_one_description">"Біреуін қайталау"</string>
<string name="exo_controls_shuffle_description">"Кездейсоқ ретпен ойнату"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ធ្វើ​ម្ដង​ទៀត​ទាំងអស់"</string>
<string name="exo_controls_repeat_off_description">"មិន​ធ្វើ​ឡើង​វិញ"</string>
<string name="exo_controls_repeat_one_description">"ធ្វើ​​ឡើងវិញ​ម្ដង"</string>
<string name="exo_controls_shuffle_description">"ច្របល់"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ"</string>
<string name="exo_controls_repeat_off_description">"ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ"</string>
<string name="exo_controls_repeat_one_description">"ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ"</string>
<string name="exo_controls_shuffle_description">"ಬೆರೆಸು"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"전체 반복"</string>
<string name="exo_controls_repeat_off_description">"반복 안함"</string>
<string name="exo_controls_repeat_one_description">"한 항목 반복"</string>
<string name="exo_controls_shuffle_description">"셔플"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Баарын кайталоо"</string>
<string name="exo_controls_repeat_off_description">"Эч бирин кайталабоо"</string>
<string name="exo_controls_repeat_one_description">"Бирөөнү кайталоо"</string>
<string name="exo_controls_shuffle_description">"Аралаштыруу"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ"</string>
<string name="exo_controls_repeat_off_description">"​ບໍ່ຫຼິ້ນ​ຊ້ຳ"</string>
<string name="exo_controls_repeat_one_description">"ຫຼິ້ນ​ຊ້ຳ"</string>
<string name="exo_controls_shuffle_description">"ຫຼີ້ນແບບສຸ່ມ"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Kartoti viską"</string>
<string name="exo_controls_repeat_off_description">"Nekartoti nieko"</string>
<string name="exo_controls_repeat_one_description">"Kartoti vieną"</string>
<string name="exo_controls_shuffle_description">"Maišyti"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Atkārtot visu"</string>
<string name="exo_controls_repeat_off_description">"Neatkārtot nevienu"</string>
<string name="exo_controls_repeat_one_description">"Atkārtot vienu"</string>
<string name="exo_controls_shuffle_description">"Atskaņot jauktā secībā"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Повтори ги сите"</string>
<string name="exo_controls_repeat_off_description">"Не повторувај ниту една"</string>
<string name="exo_controls_repeat_one_description">"Повтори една"</string>
<string name="exo_controls_shuffle_description">"По случаен избор"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"എല്ലാം ആവർത്തിക്കുക"</string>
<string name="exo_controls_repeat_off_description">"ഒന്നും ആവർത്തിക്കരുത്"</string>
<string name="exo_controls_repeat_one_description">"ഒന്ന് ആവർത്തിക്കുക"</string>
<string name="exo_controls_shuffle_description">"ഷഫിൾ ചെയ്യുക"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Бүгдийг давтах"</string>
<string name="exo_controls_repeat_off_description">"Алийг нь ч давтахгүй"</string>
<string name="exo_controls_repeat_one_description">"Нэгийг давтах"</string>
<string name="exo_controls_shuffle_description">"Холих"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"सर्व पुनरावृत्ती करा"</string>
<string name="exo_controls_repeat_off_description">"काहीही पुनरावृत्ती करू नका"</string>
<string name="exo_controls_repeat_one_description">"एक पुनरावृत्ती करा"</string>
<string name="exo_controls_shuffle_description">"शफल करा"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ulang semua"</string>
<string name="exo_controls_repeat_off_description">"Tiada ulangan"</string>
<string name="exo_controls_repeat_one_description">"Ulangan"</string>
<string name="exo_controls_shuffle_description">"Rombak"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"အားလုံး ထပ်တလဲလဲဖွင့်ရန်"</string>
<string name="exo_controls_repeat_off_description">"ထပ်တလဲလဲမဖွင့်ရန်"</string>
<string name="exo_controls_repeat_one_description">"တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်"</string>
<string name="exo_controls_shuffle_description">"မွှေနှောက်ဖွင့်ရန်"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Gjenta alle"</string>
<string name="exo_controls_repeat_off_description">"Ikke gjenta noen"</string>
<string name="exo_controls_repeat_one_description">"Gjenta én"</string>
<string name="exo_controls_shuffle_description">"Spill av i tilfeldig rekkefølge"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"सबै दोहोर्याउनुहोस्"</string>
<string name="exo_controls_repeat_off_description">"कुनै पनि नदोहोर्याउनुहोस्"</string>
<string name="exo_controls_repeat_one_description">"एउटा दोहोर्याउनुहोस्"</string>
<string name="exo_controls_shuffle_description">"मिसाउनुहोस्"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Alles herhalen"</string>
<string name="exo_controls_repeat_off_description">"Niet herhalen"</string>
<string name="exo_controls_repeat_one_description">"Eén herhalen"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ਸਭ ਨੂੰ ਦੁਹਰਾਓ"</string>
<string name="exo_controls_repeat_off_description">"ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ"</string>
<string name="exo_controls_repeat_one_description">"ਇੱਕ ਦੁਹਰਾਓ"</string>
<string name="exo_controls_shuffle_description">"ਸ਼ੱਫਲ"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Powtórz wszystkie"</string>
<string name="exo_controls_repeat_off_description">"Nie powtarzaj"</string>
<string name="exo_controls_repeat_one_description">"Powtórz jeden"</string>
<string name="exo_controls_shuffle_description">"Odtwarzaj losowo"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir tudo"</string>
<string name="exo_controls_repeat_off_description">"Não repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir um"</string>
<string name="exo_controls_shuffle_description">"Reproduzir aleatoriamente"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir tudo"</string>
<string name="exo_controls_repeat_off_description">"Não repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir um"</string>
<string name="exo_controls_shuffle_description">"Reproduzir aleatoriamente"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir tudo"</string>
<string name="exo_controls_repeat_off_description">"Não repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir uma"</string>
<string name="exo_controls_shuffle_description">"Reproduzir aleatoriamente"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetați toate"</string>
<string name="exo_controls_repeat_off_description">"Repetați niciuna"</string>
<string name="exo_controls_repeat_one_description">"Repetați unul"</string>
<string name="exo_controls_shuffle_description">"Redați aleatoriu"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Повторять все"</string>
<string name="exo_controls_repeat_off_description">"Не повторять"</string>
<string name="exo_controls_repeat_one_description">"Повторять один элемент"</string>
<string name="exo_controls_shuffle_description">"Перемешать"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"සියලු නැවත"</string>
<string name="exo_controls_repeat_off_description">"කිසිවක් නැවත"</string>
<string name="exo_controls_repeat_one_description">"නැවත නැවත එක්"</string>
<string name="exo_controls_shuffle_description">"කලවම් කරන්න"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Opakovať všetko"</string>
<string name="exo_controls_repeat_off_description">"Neopakovať"</string>
<string name="exo_controls_repeat_one_description">"Opakovať jednu položku"</string>
<string name="exo_controls_shuffle_description">"Náhodne prehrávať"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovi vse"</string>
<string name="exo_controls_repeat_off_description">"Ne ponovi"</string>
<string name="exo_controls_repeat_one_description">"Ponovi eno"</string>
<string name="exo_controls_shuffle_description">"Naključno predvajaj"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Përsërit të gjithë"</string>
<string name="exo_controls_repeat_off_description">"Përsëritni asnjë"</string>
<string name="exo_controls_repeat_one_description">"Përsëritni një"</string>
<string name="exo_controls_shuffle_description">"Përziej"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Понови све"</string>
<string name="exo_controls_repeat_off_description">"Понављање је искључено"</string>
<string name="exo_controls_repeat_one_description">"Понови једну"</string>
<string name="exo_controls_shuffle_description">"Пусти насумично"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Upprepa alla"</string>
<string name="exo_controls_repeat_off_description">"Upprepa inga"</string>
<string name="exo_controls_repeat_one_description">"Upprepa en"</string>
<string name="exo_controls_shuffle_description">"Blanda"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Rudia zote"</string>
<string name="exo_controls_repeat_off_description">"Usirudie Yoyote"</string>
<string name="exo_controls_repeat_one_description">"Rudia Moja"</string>
<string name="exo_controls_shuffle_description">"Changanya"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"அனைத்தையும் மீண்டும் இயக்கு"</string>
<string name="exo_controls_repeat_off_description">"எதையும் மீண்டும் இயக்காதே"</string>
<string name="exo_controls_repeat_one_description">"ஒன்றை மட்டும் மீண்டும் இயக்கு"</string>
<string name="exo_controls_shuffle_description">"குலை"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"అన్నీ పునరావృతం చేయి"</string>
<string name="exo_controls_repeat_off_description">"ఏదీ పునరావృతం చేయవద్దు"</string>
<string name="exo_controls_repeat_one_description">"ఒకదాన్ని పునరావృతం చేయి"</string>
<string name="exo_controls_shuffle_description">"షఫుల్ చేయి"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"เล่นซ้ำทั้งหมด"</string>
<string name="exo_controls_repeat_off_description">"ไม่เล่นซ้ำ"</string>
<string name="exo_controls_repeat_one_description">"เล่นซ้ำรายการเดียว"</string>
<string name="exo_controls_shuffle_description">"สุ่มเพลง"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ulitin Lahat"</string>
<string name="exo_controls_repeat_off_description">"Walang Uulitin"</string>
<string name="exo_controls_repeat_one_description">"Ulitin ang Isa"</string>
<string name="exo_controls_shuffle_description">"I-shuffle"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Tümünü Tekrarla"</string>
<string name="exo_controls_repeat_off_description">"Hiçbirini Tekrarlama"</string>
<string name="exo_controls_repeat_one_description">"Birini Tekrarla"</string>
<string name="exo_controls_shuffle_description">"Karıştır"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Повторити все"</string>
<string name="exo_controls_repeat_off_description">"Не повторювати"</string>
<string name="exo_controls_repeat_one_description">"Повторити один елемент"</string>
<string name="exo_controls_shuffle_description">"Перемішати"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"سبھی کو دہرائیں"</string>
<string name="exo_controls_repeat_off_description">"کسی کو نہ دہرائیں"</string>
<string name="exo_controls_repeat_one_description">"ایک کو دہرائیں"</string>
<string name="exo_controls_shuffle_description">"شفل کریں"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Barchasini takrorlash"</string>
<string name="exo_controls_repeat_off_description">"Takrorlamaslik"</string>
<string name="exo_controls_repeat_one_description">"Bir marta takrorlash"</string>
<string name="exo_controls_shuffle_description">"Tasodifiy tartibda"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Lặp lại tất cả"</string>
<string name="exo_controls_repeat_off_description">"Không lặp lại"</string>
<string name="exo_controls_repeat_one_description">"Lặp lại một mục"</string>
<string name="exo_controls_shuffle_description">"Trộn bài"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"重复播放全部"</string>
<string name="exo_controls_repeat_off_description">"不重复播放"</string>
<string name="exo_controls_repeat_one_description">"重复播放单个视频"</string>
<string name="exo_controls_shuffle_description">"随机播放"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"重複播放所有媒體項目"</string>
<string name="exo_controls_repeat_off_description">"不重複播放任何媒體項目"</string>
<string name="exo_controls_repeat_one_description">"重複播放一個媒體項目"</string>
<string name="exo_controls_shuffle_description">"隨機播放"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"重複播放所有媒體項目"</string>
<string name="exo_controls_repeat_off_description">"不重複播放"</string>
<string name="exo_controls_repeat_one_description">"重複播放單一媒體項目"</string>
<string name="exo_controls_shuffle_description">"隨機播放"</string>
</resources>
......@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Phinda konke"</string>
<string name="exo_controls_repeat_off_description">"Ungaphindi lutho"</string>
<string name="exo_controls_repeat_one_description">"Phida okukodwa"</string>
<string name="exo_controls_shuffle_description">"Shova"</string>
</resources>
......@@ -28,6 +28,7 @@
<item name="exo_ffwd" type="id"/>
<item name="exo_prev" type="id"/>
<item name="exo_next" type="id"/>
<item name="exo_shuffle" type="id"/>
<item name="exo_repeat_toggle" type="id"/>
<item name="exo_duration" type="id"/>
<item name="exo_position" type="id"/>
......
......@@ -24,4 +24,5 @@
<string name="exo_controls_repeat_off_description">Repeat none</string>
<string name="exo_controls_repeat_one_description">Repeat one</string>
<string name="exo_controls_repeat_all_description">Repeat all</string>
<string name="exo_controls_shuffle_description">Shuffle</string>
</resources>
......@@ -51,4 +51,9 @@
<item name="android:contentDescription">@string/exo_controls_pause_description</item>
</style>
<style name="ExoMediaButton.Shuffle">
<item name="android:src">@drawable/exo_controls_shuffle</item>
<item name="android:contentDescription">@string/exo_controls_shuffle_description</item>
</style>
</resources>
......@@ -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'
......@@ -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);
doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction);
}
/**
* 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 #doAction(SimpleExoPlayer, MappingTrackSelector, Surface)} do perform the
* action.
* 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) {
}
}
}
......@@ -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);
}
......
......@@ -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<FrameworkMediaCrypto> buildDrmSessionManager(String userAgent) {
// Do nothing. Interested subclasses may override.
return null;
......
/*
* 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<Timeline> timelines;
private final LinkedList<Object> manifests;
private final LinkedList<Integer> 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.
}
}
/*
* 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<Pair<Timeline, Object>> 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<Timeline, Object>... sourceInfos) {
Assert.assertEquals(sourceInfos.length, this.sourceInfos.size());
for (Pair<Timeline, Object> 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.
}
}
......@@ -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) {
......
/*
* 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<FakeChunkSource>}.
*/
public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod
implements SequenceableLoader.Callback<ChunkSampleStream<FakeChunkSource>> {
private final EventDispatcher eventDispatcher;
private final Allocator allocator;
private final FakeChunkSource.Factory chunkSourceFactory;
private final long durationUs;
private Callback callback;
private ChunkSampleStream<FakeChunkSource>[] 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<ChunkSampleStream<FakeChunkSource>> validStreams = new ArrayList<>();
for (SampleStream stream : streams) {
if (stream != null) {
validStreams.add((ChunkSampleStream<FakeChunkSource>) 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<FakeChunkSource> 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<FakeChunkSource> source) {
callback.onContinueLoadingRequested(this);
}
}
/*
* 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);
}
}
......@@ -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);
......
......@@ -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);
......
......@@ -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);
}
......
......@@ -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());
}
}
......@@ -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<FakeMediaPeriod> 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++) {
......
......@@ -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;
}
}
/*
* 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<Player.EventListener> 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);
}
}
});
}
}
}
......@@ -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.
* <p>
* 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
......@@ -177,12 +173,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba
}
@Override
public void onResume() {
super.onResume();
maybeStartHostedTest();
}
@Override
public void onPause() {
super.onPause();
maybeStopHostedTest();
......@@ -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);
}
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment