Commit 0425ff6d by Oliver Woodman

Merge branch 'dev-v2' into release-v2

parents deb9b301 427411be
Showing with 897 additions and 235 deletions
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../constants.gradle' apply from: '../../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
android { android {
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertArrayEquals;
......
...@@ -472,13 +472,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -472,13 +472,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
} }
if (!imaPlayingAd) { if (!imaPlayingAd) {
imaPlayingAd = true; imaPlayingAd = true;
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onPlay(); adCallbacks.get(i).onPlay();
} }
} else if (imaPausedInAd) { } else if (imaPausedInAd) {
imaPausedInAd = false; imaPausedInAd = false;
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onResume(); adCallbacks.get(i).onResume();
} }
} }
} }
...@@ -509,8 +509,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -509,8 +509,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
return; return;
} }
imaPausedInAd = true; imaPausedInAd = true;
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onPause(); adCallbacks.get(i).onPause();
} }
} }
...@@ -555,8 +555,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -555,8 +555,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
} else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) {
// IMA is waiting for the ad playback to finish so invoke the callback now. // 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. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onEnded(); adCallbacks.get(i).onEnded();
} }
} }
} }
...@@ -569,8 +569,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -569,8 +569,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
@Override @Override
public void onPlayerError(ExoPlaybackException error) { public void onPlayerError(ExoPlaybackException error) {
if (playingAd) { if (playingAd) {
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onError(); adCallbacks.get(i).onError();
} }
} }
} }
...@@ -630,8 +630,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ...@@ -630,8 +630,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
if (adFinished) { if (adFinished) {
// IMA is waiting for the ad playback to finish so invoke the callback now. // 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. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
for (VideoAdPlayerCallback callback : adCallbacks) { for (int i = 0; i < adCallbacks.size(); i++) {
callback.onEnded(); adCallbacks.get(i).onEnded();
} }
} }
if (!wasPlayingAd && playingAd) { if (!wasPlayingAd && playingAd) {
......
...@@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima; ...@@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
/** /**
* A {@link Timeline} for sources that have ads. * 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 long[] adGroupTimesUs;
private final int[] adCounts; private final int[] adCounts;
private final int[] adsLoadedCounts; private final int[] adsLoadedCounts;
...@@ -52,9 +52,9 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -52,9 +52,9 @@ import com.google.android.exoplayer2.util.Assertions;
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs,
long adResumePositionUs) { long adResumePositionUs) {
super(contentTimeline);
Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1);
this.contentTimeline = contentTimeline;
this.adGroupTimesUs = adGroupTimesUs; this.adGroupTimesUs = adGroupTimesUs;
this.adCounts = adCounts; this.adCounts = adCounts;
this.adsLoadedCounts = adsLoadedCounts; this.adsLoadedCounts = adsLoadedCounts;
...@@ -64,33 +64,12 @@ import com.google.android.exoplayer2.util.Assertions; ...@@ -64,33 +64,12 @@ import com.google.android.exoplayer2.util.Assertions;
} }
@Override @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) { 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.set(period.id, period.uid, period.windowIndex, period.durationUs,
period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts, period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts,
adDurationsUs, adResumePositionUs); adDurationsUs, adResumePositionUs);
return period; return period;
} }
@Override
public int getIndexOfPeriod(Object uid) {
return contentTimeline.getIndexOfPeriod(uid);
}
} }
...@@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession; ...@@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession;
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.media.session.PlaybackStateCompat; import android.support.v4.media.session.PlaybackStateCompat;
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
......
...@@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
TestUtil.buildTestData(4000, random), TestUtil.buildTestData(4000, random),
new byte[]{'O', 'g', 'g', 'S'}, new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random) TestUtil.buildTestData(4000, random)
), false); ), false);
skipToNextPage(extractorInput); skipToNextPage(extractorInput);
...@@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
TestUtil.buildTestData(2046, random), TestUtil.buildTestData(2046, random),
new byte[]{'O', 'g', 'g', 'S'}, new byte[] {'O', 'g', 'g', 'S'},
TestUtil.buildTestData(4000, random) TestUtil.buildTestData(4000, random)
), false); ), false);
skipToNextPage(extractorInput); skipToNextPage(extractorInput);
...@@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
new byte[]{'x', 'O', 'g', 'g', 'S'} new byte[] {'x', 'O', 'g', 'g', 'S'}
), false); ), false);
skipToNextPage(extractorInput); skipToNextPage(extractorInput);
assertEquals(1, extractorInput.getPosition()); assertEquals(1, extractorInput.getPosition());
...@@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
public void testSkipToNextPageNoMatch() throws Exception { public void testSkipToNextPageNoMatch() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false); new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
try { try {
skipToNextPage(extractorInput); skipToNextPage(extractorInput);
fail(); fail();
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; 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.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
...@@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase { ...@@ -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); data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);
feed(); feed();
assertSampleCounts(0, 1); assertSampleCounts(0, 1);
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null); 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(); maybeStartPacket();
data.setLimit(limit); data.setLimit(limit);
feed(); feed();
} }
private void feed() { private void feed() throws ParserException {
maybeStartPacket(); maybeStartPacket();
adtsReader.consume(data); adtsReader.consume(data);
} }
......
...@@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase { ...@@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase {
public void testAdvanceToBeforeBuffer() { public void testAdvanceToBeforeBuffer() {
writeTestData(); 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. // Should fail and have no effect.
assertFalse(result); assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
assertReadTestData(); assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2);
} }
public void testAdvanceToStartOfBuffer() { public void testAdvanceToStartOfBuffer() {
writeTestData(); 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). // Should succeed but have no effect (we're already at the first frame).
assertTrue(result); assertEquals(0, skipCount);
assertReadTestData(); assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2);
} }
public void testAdvanceToEndOfBuffer() { public void testAdvanceToEndOfBuffer() {
writeTestData(); writeTestData();
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
// Should succeed and skip to 2nd keyframe. // Should succeed and skip to 2nd keyframe (the 4th frame).
assertTrue(result); assertEquals(4, skipCount);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2);
} }
public void testAdvanceToAfterBuffer() { public void testAdvanceToAfterBuffer() {
writeTestData(); 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. // Should fail and have no effect.
assertFalse(result); assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
assertReadTestData(); assertReadTestData();
assertNoSamplesToRead(TEST_FORMAT_2); assertNoSamplesToRead(TEST_FORMAT_2);
} }
public void testAdvanceToAfterBufferAllowed() { public void testAdvanceToAfterBufferAllowed() {
writeTestData(); writeTestData();
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
// Should succeed and skip to 2nd keyframe. // Should succeed and skip to 2nd keyframe (the 4th frame).
assertTrue(result); assertEquals(4, skipCount);
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
assertNoSamplesToRead(TEST_FORMAT_2); 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 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
......
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
...@@ -27,8 +26,14 @@ public final class ParsableBitArrayTest extends 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, private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01,
(byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99};
private ParsableBitArray testArray;
@Override
public void setUp() {
testArray = new ParsableBitArray(TEST_DATA);
}
public void testReadAllBytes() { public void testReadAllBytes() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
byte[] bytesRead = new byte[TEST_DATA.length]; byte[] bytesRead = new byte[TEST_DATA.length];
testArray.readBytes(bytesRead, 0, TEST_DATA.length); testArray.readBytes(bytesRead, 0, TEST_DATA.length);
MoreAsserts.assertEquals(TEST_DATA, bytesRead); MoreAsserts.assertEquals(TEST_DATA, bytesRead);
...@@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase { ...@@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase {
} }
public void testReadBit() { public void testReadBit() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertReadBitsToEnd(0);
assertReadBitsToEnd(0, testArray);
} }
public void testReadBits() { public void testReadBits() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
assertEquals(getTestDataBits(5, 0), testArray.readBits(0));
assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); assertEquals(getTestDataBits(5, 3), testArray.readBits(3));
assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); assertEquals(getTestDataBits(8, 16), testArray.readBits(16));
assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); assertEquals(getTestDataBits(24, 3), testArray.readBits(3));
...@@ -52,67 +56,101 @@ public final class ParsableBitArrayTest extends TestCase { ...@@ -52,67 +56,101 @@ public final class ParsableBitArrayTest extends TestCase {
assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); 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() { public void testRead32BitsByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); assertEquals(getTestDataBits(0, 32), testArray.readBits(32));
assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); assertEquals(getTestDataBits(32, 32), testArray.readBits(32));
} }
public void testRead32BitsNonByteAligned() { public void testRead32BitsNonByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); assertEquals(getTestDataBits(5, 32), testArray.readBits(32));
} }
public void testSkipBytes() { public void testSkipBytes() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBytes(2); testArray.skipBytes(2);
assertReadBitsToEnd(16, testArray); assertReadBitsToEnd(16);
} }
public void testSkipBitsByteAligned() { public void testSkipBitsByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBits(16); testArray.skipBits(16);
assertReadBitsToEnd(16, testArray); assertReadBitsToEnd(16);
} }
public void testSkipBitsNonByteAligned() { public void testSkipBitsNonByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.skipBits(5); testArray.skipBits(5);
assertReadBitsToEnd(5, testArray); assertReadBitsToEnd(5);
} }
public void testSetPositionByteAligned() { public void testSetPositionByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(16); testArray.setPosition(16);
assertReadBitsToEnd(16, testArray); assertReadBitsToEnd(16);
} }
public void testSetPositionNonByteAligned() { public void testSetPositionNonByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(5); testArray.setPosition(5);
assertReadBitsToEnd(5, testArray); assertReadBitsToEnd(5);
} }
public void testByteAlignFromNonByteAligned() { public void testByteAlignFromNonByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(11); testArray.setPosition(11);
testArray.byteAlign(); testArray.byteAlign();
assertEquals(2, testArray.getBytePosition()); assertEquals(2, testArray.getBytePosition());
assertEquals(16, testArray.getPosition()); assertEquals(16, testArray.getPosition());
assertReadBitsToEnd(16, testArray); assertReadBitsToEnd(16);
} }
public void testByteAlignFromByteAligned() { public void testByteAlignFromByteAligned() {
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
testArray.setPosition(16); testArray.setPosition(16);
testArray.byteAlign(); // Should be a no-op. testArray.byteAlign(); // Should be a no-op.
assertEquals(2, testArray.getBytePosition()); assertEquals(2, testArray.getBytePosition());
assertEquals(16, testArray.getPosition()); 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(); int position = testArray.getPosition();
assertEquals(expectedStartPosition, position); assertEquals(expectedStartPosition, position);
for (int i = position; i < TEST_DATA.length * 8; i++) { for (int i = position; i < TEST_DATA.length * 8; i++) {
......
...@@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase { ...@@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase {
} }
public void testReadLittleEndianLong() { public void testReadLittleEndianLong() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, (byte) 0xFF 0x00, 0x00, 0x00, (byte) 0xFF
}); });
...@@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase { ...@@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase {
} }
public void testReadLittleEndianInt() { public void testReadLittleEndianInt() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, 0x00, 0x00, (byte) 0xFF 0x01, 0x00, 0x00, (byte) 0xFF
}); });
assertEquals(0xFF000001, byteArray.readLittleEndianInt()); assertEquals(0xFF000001, byteArray.readLittleEndianInt());
...@@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase { ...@@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase {
} }
public void testReadLittleEndianUnsignedShort() { public void testReadLittleEndianUnsignedShort() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, (byte) 0xFF, 0x02, (byte) 0xFF 0x01, (byte) 0xFF, 0x02, (byte) 0xFF
}); });
assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort()); assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort());
...@@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase { ...@@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase {
} }
public void testReadLittleEndianShort() { public void testReadLittleEndianShort() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
0x01, (byte) 0xFF, 0x02, (byte) 0xFF 0x01, (byte) 0xFF, 0x02, (byte) 0xFF
}); });
assertEquals((short) 0xFF01, byteArray.readLittleEndianShort()); assertEquals((short) 0xFF01, byteArray.readLittleEndianShort());
......
...@@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* {@code positionUs} is beyond it. * {@code positionUs} is beyond it.
* *
* @param positionUs The position in microseconds. * @param positionUs The position in microseconds.
* @return The number of samples that were skipped.
*/ */
protected void skipSource(long positionUs) { protected int skipSource(long positionUs) {
stream.skipData(positionUs - streamOffsetUs); return stream.skipData(positionUs - streamOffsetUs);
} }
/** /**
......
...@@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer {
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects. // 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 { ...@@ -723,6 +723,19 @@ public class SimpleExoPlayer implements ExoPlayer {
// Internal methods. // 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() { private void removeSurfaceCallbacks() {
if (textureView != null) { if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) { if (textureView.getSurfaceTextureListener() != componentListener) {
......
...@@ -37,6 +37,12 @@ public final class DecoderCounters { ...@@ -37,6 +37,12 @@ public final class DecoderCounters {
*/ */
public int inputBufferCount; 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. * The number of rendered output buffers.
*/ */
public int renderedOutputBufferCount; public int renderedOutputBufferCount;
...@@ -79,6 +85,7 @@ public final class DecoderCounters { ...@@ -79,6 +85,7 @@ public final class DecoderCounters {
decoderInitCount += other.decoderInitCount; decoderInitCount += other.decoderInitCount;
decoderReleaseCount += other.decoderReleaseCount; decoderReleaseCount += other.decoderReleaseCount;
inputBufferCount += other.inputBufferCount; inputBufferCount += other.inputBufferCount;
skippedInputBufferCount += other.skippedInputBufferCount;
renderedOutputBufferCount += other.renderedOutputBufferCount; renderedOutputBufferCount += other.renderedOutputBufferCount;
skippedOutputBufferCount += other.skippedOutputBufferCount; skippedOutputBufferCount += other.skippedOutputBufferCount;
droppedOutputBufferCount += other.droppedOutputBufferCount; droppedOutputBufferCount += other.droppedOutputBufferCount;
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.media.MediaDrm; import android.media.MediaDrm;
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
...@@ -85,7 +86,7 @@ import java.util.Collections; ...@@ -85,7 +86,7 @@ import java.util.Collections;
} }
@Override @Override
protected void parsePayload(ParsableByteArray data, long timeUs) { protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
if (audioFormat == AUDIO_FORMAT_MP3) { if (audioFormat == AUDIO_FORMAT_MP3) {
int sampleSize = data.bytesLeft(); int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize); output.sampleData(data, sampleSize);
......
...@@ -816,7 +816,7 @@ import java.util.List; ...@@ -816,7 +816,7 @@ import java.util.List;
private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position, private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData, 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); parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
int quickTimeSoundDescriptionVersion = 0; int quickTimeSoundDescriptionVersion = 0;
......
...@@ -19,6 +19,7 @@ import android.util.Log; ...@@ -19,6 +19,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; 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.DummyTrackOutput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
...@@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader { ...@@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader {
} }
@Override @Override
public void consume(ParsableByteArray data) { public void consume(ParsableByteArray data) throws ParserException {
while (data.bytesLeft() > 0) { while (data.bytesLeft() > 0) {
switch (state) { switch (state) {
case STATE_FINDING_SAMPLE: case STATE_FINDING_SAMPLE:
...@@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader { ...@@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader {
/** /**
* Parses the sample header. * Parses the sample header.
*/ */
private void parseAdtsHeader() { private void parseAdtsHeader() throws ParserException {
adtsScratch.setPosition(0); adtsScratch.setPosition(0);
if (!hasOutputFormat) { if (!hasOutputFormat) {
......
...@@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact ...@@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA:
case TsExtractor.TS_STREAM_TYPE_MPA_LSF: case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
return new PesReader(new MpegAudioReader(esInfo.language)); 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) return isSet(FLAG_IGNORE_AAC_STREAM)
? null : new PesReader(new AdtsReader(false, esInfo.language)); ? 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_AC3:
case TsExtractor.TS_STREAM_TYPE_E_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3:
return new PesReader(new Ac3Reader(esInfo.language)); return new PesReader(new Ac3Reader(esInfo.language));
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; 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.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -50,8 +51,9 @@ public interface ElementaryStreamReader { ...@@ -50,8 +51,9 @@ public interface ElementaryStreamReader {
* Consumes (possibly partial) data from the current packet. * Consumes (possibly partial) data from the current packet.
* *
* @param data The data to consume. * @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. * Called when a packet ends.
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader { ...@@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader {
} }
@Override @Override
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
throws ParserException {
if (payloadUnitStartIndicator) { if (payloadUnitStartIndicator) {
switch (state) { switch (state) {
case STATE_FINDING_HEADER: case STATE_FINDING_HEADER:
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; 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.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
...@@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; ...@@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException; 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 { public final class PsExtractor implements Extractor {
...@@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor { ...@@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor {
* Consumes the payload of a PS packet. * Consumes the payload of a PS packet.
* *
* @param data The PES packet. The position will be set to the start of the payload. * @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); data.readBytes(pesScratch.data, 0, 3);
pesScratch.setPosition(0); pesScratch.setPosition(0);
parseHeader(); parseHeader();
......
...@@ -84,7 +84,8 @@ public final class TsExtractor implements Extractor { ...@@ -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 = 0x03;
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; 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_AC3 = 0x81;
public static final int TS_STREAM_TYPE_DTS = 0x8A; public static final int TS_STREAM_TYPE_DTS = 0x8A;
public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -196,7 +197,8 @@ public interface TsPayloadReader { ...@@ -196,7 +197,8 @@ public interface TsPayloadReader {
* *
* @param data The TS packet. The position will be set to the start of the payload. * @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. * @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 { ...@@ -530,7 +530,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
TraceUtil.endSection(); TraceUtil.endSection();
} else { } else {
skipSource(positionUs); decoderCounters.skippedInputBufferCount += skipSource(positionUs);
// We need to read any format changes despite not having a codec so that drmSession can be // 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 // 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 // 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 ...@@ -286,8 +286,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
stream.skipData(startUs + positionUs); return stream.skipData(startUs + positionUs);
} }
} }
......
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -128,9 +127,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ...@@ -128,9 +127,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
/** /**
* Provides a clipped view of a specified timeline. * 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 startUs;
private final long endUs; private final long endUs;
...@@ -143,6 +141,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ...@@ -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. * 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) { public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
super(timeline);
Assertions.checkArgument(timeline.getWindowCount() == 1); Assertions.checkArgument(timeline.getWindowCount() == 1);
Assertions.checkArgument(timeline.getPeriodCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1);
Window window = timeline.getWindow(0, new Window(), false); Window window = timeline.getWindow(0, new Window(), false);
...@@ -155,27 +154,11 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ...@@ -155,27 +154,11 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
} }
Period period = timeline.getPeriod(0, new Period()); Period period = timeline.getPeriod(0, new Period());
Assertions.checkArgument(period.getPositionInWindowUs() == 0); Assertions.checkArgument(period.getPositionInWindowUs() == 0);
this.timeline = timeline;
this.startUs = startUs; this.startUs = startUs;
this.endUs = resolvedEndUs; this.endUs = resolvedEndUs;
} }
@Override @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, public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) { long defaultPositionProjectionUs) {
window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs); window = timeline.getWindow(0, window, setIds, defaultPositionProjectionUs);
...@@ -197,22 +180,12 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ...@@ -197,22 +180,12 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
} }
@Override @Override
public int getPeriodCount() {
return 1;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) { public Period getPeriod(int periodIndex, Period period, boolean setIds) {
period = timeline.getPeriod(0, period, setIds); period = timeline.getPeriod(0, period, setIds);
period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET; period.durationUs = endUs != C.TIME_UNSET ? endUs - startUs : C.TIME_UNSET;
return period; return period;
} }
@Override
public int getIndexOfPeriod(Object uid) {
return timeline.getIndexOfPeriod(uid);
}
} }
} }
...@@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl ...@@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() throws IOException {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { for (int i = 0; i < mediaSourceHolders.size(); i++) {
mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError(); mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError();
} }
} }
...@@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl ...@@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override @Override
public void releaseSource() { public void releaseSource() {
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { for (int i = 0; i < mediaSourceHolders.size(); i++) {
mediaSourceHolder.mediaSource.releaseSource(); mediaSourceHolders.get(i).mediaSource.releaseSource();
} }
} }
......
...@@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream { ...@@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream {
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
// Do nothing. return 0;
} }
} }
...@@ -238,7 +238,7 @@ import java.util.Arrays; ...@@ -238,7 +238,7 @@ import java.util.Arrays;
// sample queue, or if we haven't read anything from the queue since the previous seek // 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 // (this case is common for sparse tracks such as metadata tracks). In all other cases a
// seek is required. // seek is required.
seekRequired = !sampleQueue.advanceTo(positionUs, true, true) seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
&& sampleQueue.getReadIndex() != 0; && sampleQueue.getReadIndex() != 0;
} }
} }
...@@ -371,12 +371,13 @@ import java.util.Arrays; ...@@ -371,12 +371,13 @@ import java.util.Arrays;
lastSeekPositionUs); lastSeekPositionUs);
} }
/* package */ void skipData(int track, long positionUs) { /* package */ int skipData(int track, long positionUs) {
SampleQueue sampleQueue = sampleQueues[track]; SampleQueue sampleQueue = sampleQueues[track];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
sampleQueue.advanceToEnd(); return sampleQueue.advanceToEnd();
} else { } 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; ...@@ -558,7 +559,8 @@ import java.util.Arrays;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
SampleQueue sampleQueue = sampleQueues[i]; SampleQueue sampleQueue = sampleQueues[i];
sampleQueue.rewind(); 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 // 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 // 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 // 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; ...@@ -632,8 +634,8 @@ import java.util.Arrays;
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
ExtractorMediaPeriod.this.skipData(track, 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 { ...@@ -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 timeline) {
super(timeline);
public InfinitelyLoopingTimeline(Timeline childTimeline) {
this.childTimeline = childTimeline;
}
@Override
public int getWindowCount() {
return childTimeline.getWindowCount();
} }
@Override @Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { 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; return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
} }
@Override @Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { 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 return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
: childPreviousWindowIndex; : 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; ...@@ -253,32 +253,35 @@ import com.google.android.exoplayer2.util.Util;
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * @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 * end of the queue, by advancing the read position to the last sample (or keyframe) in the
* queue. * queue.
* @return Whether the operation was a success. A successful advance is one in which the read * @return The number of samples that were skipped if the operation was successful, which may be
* position was unchanged or advanced, and is now at a sample meeting the specified criteria. * 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) { boolean allowTimeBeyondBuffer) {
int relativeReadIndex = getRelativeIndex(readPosition); int relativeReadIndex = getRelativeIndex(readPosition);
if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]
|| (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
return false; return SampleQueue.ADVANCE_FAILED;
} }
int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);
if (offset == -1) { if (offset == -1) {
return false; return SampleQueue.ADVANCE_FAILED;
} }
readPosition += offset; readPosition += offset;
return true; return offset;
} }
/** /**
* Advances the read position to the end of the queue. * Advances the read position to the end of the queue.
*
* @return The number of samples that were skipped.
*/ */
public synchronized void advanceToEnd() { public synchronized int advanceToEnd() {
if (!hasNextSample()) { int skipCount = length - readPosition;
return;
}
readPosition = length; readPosition = length;
return skipCount;
} }
/** /**
......
...@@ -49,6 +49,8 @@ public final class SampleQueue implements TrackOutput { ...@@ -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 static final int INITIAL_SCRATCH_SIZE = 32;
private final Allocator allocator; private final Allocator allocator;
...@@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput { ...@@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput {
/** /**
* Advances the read position to the end of the queue. * Advances the read position to the end of the queue.
*
* @return The number of samples that were skipped.
*/ */
public void advanceToEnd() { public int advanceToEnd() {
metadataQueue.advanceToEnd(); return metadataQueue.advanceToEnd();
} }
/** /**
...@@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput { ...@@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput {
* time, rather than to any sample before or at that time. * time, rather than to any sample before or at that time.
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * @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). * 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 * @return The number of samples that were skipped if the operation was successful, which may be
* position was unchanged or advanced, and is now at a sample meeting the specified criteria. * 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); return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
} }
......
...@@ -70,7 +70,8 @@ public interface SampleStream { ...@@ -70,7 +70,8 @@ public interface SampleStream {
* {@code positionUs} is beyond it. * {@code positionUs} is beyond it.
* *
* @param positionUs The specified time. * @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; ...@@ -235,10 +235,12 @@ import java.util.Arrays;
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
if (positionUs > 0) { if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {
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 ...@@ -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 * @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. * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
*/ */
@Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
if (loadingFinished) { if (loadingFinished) {
return C.TIME_END_OF_SOURCE; return C.TIME_END_OF_SOURCE;
...@@ -185,8 +186,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S ...@@ -185,8 +186,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the primary sample queue. // If we're not pending a reset, see if we can seek within the primary sample queue.
boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true, boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true,
positionUs < getNextLoadPositionUs()); positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED);
if (seekInsideBuffer) { if (seekInsideBuffer) {
// We succeeded. Discard samples and corresponding chunks prior to the seek position. // We succeeded. Discard samples and corresponding chunks prior to the seek position.
discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
...@@ -266,13 +267,19 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S ...@@ -266,13 +267,19 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
int skipCount;
if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
primarySampleQueue.advanceToEnd(); primarySampleQueue.advanceToEnd();
skipCount = primarySampleQueue.advanceToEnd();
} else { } else {
primarySampleQueue.advanceTo(positionUs, true, true); skipCount = primarySampleQueue.advanceTo(positionUs, true, true);
if (skipCount == SampleQueue.ADVANCE_FAILED) {
skipCount = 0;
}
} }
primarySampleQueue.discardToRead(); primarySampleQueue.discardToRead();
return skipCount;
} }
// Loader.Callback implementation. // Loader.Callback implementation.
...@@ -470,11 +477,12 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S ...@@ -470,11 +477,12 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
sampleQueue.advanceToEnd(); return sampleQueue.advanceToEnd();
} else { } 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 { ...@@ -811,43 +811,43 @@ public final class Cea708Decoder extends CeaDecoder {
private static final int PEN_OFFSET_NORMAL = 1; private static final int PEN_OFFSET_NORMAL = 1;
// The window style properties are specified in the CEA-708 specification. // 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_LEFT,
JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
JUSTIFICATION_LEFT 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_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 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_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 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 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_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, 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. // 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_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_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_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_NONE,
BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
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_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
......
...@@ -34,9 +34,9 @@ import java.io.OutputStream; ...@@ -34,9 +34,9 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import javax.crypto.Cipher; import javax.crypto.Cipher;
...@@ -176,14 +176,14 @@ import javax.crypto.spec.SecretKeySpec; ...@@ -176,14 +176,14 @@ import javax.crypto.spec.SecretKeySpec;
/** Removes empty {@link CachedContent} instances from index. */ /** Removes empty {@link CachedContent} instances from index. */
public void removeEmpty() { public void removeEmpty() {
LinkedList<String> cachedContentToBeRemoved = new LinkedList<>(); ArrayList<String> cachedContentToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : keyToContent.values()) { for (CachedContent cachedContent : keyToContent.values()) {
if (cachedContent.isEmpty()) { if (cachedContent.isEmpty()) {
cachedContentToBeRemoved.add(cachedContent.key); cachedContentToBeRemoved.add(cachedContent.key);
} }
} }
for (String key : cachedContentToBeRemoved) { for (int i = 0; i < cachedContentToBeRemoved.size(); i++) {
removeEmpty(key); removeEmpty(cachedContentToBeRemoved.get(i));
} }
} }
......
...@@ -22,7 +22,6 @@ import java.io.File; ...@@ -22,7 +22,6 @@ import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList;
import java.util.NavigableSet; import java.util.NavigableSet;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
...@@ -308,7 +307,7 @@ public final class SimpleCache implements Cache { ...@@ -308,7 +307,7 @@ public final class SimpleCache implements Cache {
* no longer exist. * no longer exist.
*/ */
private void removeStaleSpansAndCachedContents() throws CacheException { private void removeStaleSpansAndCachedContents() throws CacheException {
LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>(); ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
for (CachedContent cachedContent : index.getAll()) { for (CachedContent cachedContent : index.getAll()) {
for (CacheSpan span : cachedContent.getSpans()) { for (CacheSpan span : cachedContent.getSpans()) {
if (!span.file.exists()) { if (!span.file.exists()) {
...@@ -316,9 +315,9 @@ public final class SimpleCache implements Cache { ...@@ -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. // Remove span but not CachedContent to prevent multiple index.store() calls.
removeSpan(span, false); removeSpan(spansToBeRemoved.get(i), false);
} }
index.removeEmpty(); index.removeEmpty();
index.store(); index.store();
......
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util;
import android.util.Pair; import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
...@@ -83,11 +84,27 @@ public final class CodecSpecificDataUtil { ...@@ -83,11 +84,27 @@ public final class CodecSpecificDataUtil {
/** /**
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * 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. * @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) { public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig)
ParsableBitArray bitArray = new ParsableBitArray(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 audioObjectType = getAacAudioObjectType(bitArray);
int sampleRate = getAacSamplingFrequency(bitArray); int sampleRate = getAacSamplingFrequency(bitArray);
int channelConfiguration = bitArray.readBits(4); int channelConfiguration = bitArray.readBits(4);
...@@ -104,6 +121,41 @@ public final class CodecSpecificDataUtil { ...@@ -104,6 +121,41 @@ public final class CodecSpecificDataUtil {
channelConfiguration = bitArray.readBits(4); 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]; int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];
Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);
return Pair.create(sampleRate, channelCount); return Pair.create(sampleRate, channelCount);
...@@ -269,4 +321,32 @@ public final class CodecSpecificDataUtil { ...@@ -269,4 +321,32 @@ public final class CodecSpecificDataUtil {
return samplingFrequency; 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 { ...@@ -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. * Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
*/ */
public void byteAlign() { public void byteAlign() {
......
...@@ -76,7 +76,7 @@ public final class DashUtilTest extends TestCase { ...@@ -76,7 +76,7 @@ public final class DashUtilTest extends TestCase {
private static DrmInitData newDrmInitData() { private static DrmInitData newDrmInitData() {
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", 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 { ...@@ -35,6 +35,7 @@ android {
dependencies { dependencies {
compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-core')
compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile project(modulePrefix + 'testutils')
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
......
...@@ -50,8 +50,8 @@ import java.io.IOException; ...@@ -50,8 +50,8 @@ import java.io.IOException;
} }
@Override @Override
public void skipData(long positionUs) { public int skipData(long positionUs) {
sampleStreamWrapper.skipData(group, positionUs); return sampleStreamWrapper.skipData(group, positionUs);
} }
} }
...@@ -229,7 +229,7 @@ import java.util.LinkedList; ...@@ -229,7 +229,7 @@ import java.util.LinkedList;
// sample queue, or if we haven't read anything from the queue since the previous seek // 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 // (this case is common for sparse tracks such as metadata tracks). In all other cases a
// seek is required. // seek is required.
seekRequired = !sampleQueue.advanceTo(positionUs, true, true) seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
&& sampleQueue.getReadIndex() != 0; && sampleQueue.getReadIndex() != 0;
} }
} }
...@@ -320,6 +320,7 @@ import java.util.LinkedList; ...@@ -320,6 +320,7 @@ import java.util.LinkedList;
return true; return true;
} }
@Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
if (loadingFinished) { if (loadingFinished) {
return C.TIME_END_OF_SOURCE; return C.TIME_END_OF_SOURCE;
...@@ -402,12 +403,13 @@ import java.util.LinkedList; ...@@ -402,12 +403,13 @@ import java.util.LinkedList;
lastSeekPositionUs); lastSeekPositionUs);
} }
/* package */ void skipData(int trackGroupIndex, long positionUs) { /* package */ int skipData(int trackGroupIndex, long positionUs) {
SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
sampleQueue.advanceToEnd(); return sampleQueue.advanceToEnd();
} else { } 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; ...@@ -760,7 +762,8 @@ import java.util.LinkedList;
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
SampleQueue sampleQueue = sampleQueues[i]; SampleQueue sampleQueue = sampleQueues[i];
sampleQueue.rewind(); 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 // 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 // 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 // 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; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
...@@ -109,6 +110,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ...@@ -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. * Creates a playlist with a single variant.
* *
* @param variantUrl The url of the single variant. * @param variantUrl The url of the single variant.
...@@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ...@@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
emptyList, null, null); 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 ...@@ -186,8 +186,9 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener
return ""; return "";
} }
counters.ensureUpdated(); counters.ensureUpdated();
return " rb:" + counters.renderedOutputBufferCount return " sib:" + counters.skippedInputBufferCount
+ " sb:" + counters.skippedOutputBufferCount + " sb:" + counters.skippedOutputBufferCount
+ " rb:" + counters.renderedOutputBufferCount
+ " db:" + counters.droppedOutputBufferCount + " db:" + counters.droppedOutputBufferCount
+ " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; + " 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 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Herhaal alles"</string> <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_off_description">"Herhaal niks"</string>
<string name="exo_controls_repeat_one_description">"Herhaal een"</string> <string name="exo_controls_repeat_one_description">"Herhaal een"</string>
<string name="exo_controls_shuffle_description">"Skommel"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"ሁሉንም ድገም"</string> <string name="exo_controls_repeat_all_description">"ሁሉንም ድገም"</string>
<string name="exo_controls_repeat_off_description">"ምንም አትድገም"</string> <string name="exo_controls_repeat_off_description">"ምንም አትድገም"</string>
<string name="exo_controls_repeat_one_description">"አንዱን ድገም"</string> <string name="exo_controls_repeat_one_description">"አንዱን ድገም"</string>
<string name="exo_controls_shuffle_description">"በው"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"تكرار الكل"</string> <string name="exo_controls_repeat_all_description">"تكرار الكل"</string>
<string name="exo_controls_repeat_off_description">"عدم التكرار"</string> <string name="exo_controls_repeat_off_description">"عدم التكرار"</string>
<string name="exo_controls_repeat_one_description">"تكرار مقطع واحد"</string> <string name="exo_controls_repeat_one_description">"تكرار مقطع واحد"</string>
<string name="exo_controls_shuffle_description">"ترتيب عشوائي"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Bütün təkrarlayın"</string> <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_one_description">"Təkrar bir"</string>
<string name="exo_controls_repeat_off_description">"Heç bir təkrar"</string> <string name="exo_controls_repeat_off_description">"Heç bir təkrar"</string>
<string name="exo_controls_shuffle_description">"Qarışdır"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovi sve"</string> <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_off_description">"Ne ponavljaj nijednu"</string>
<string name="exo_controls_repeat_one_description">"Ponovi jednu"</string> <string name="exo_controls_repeat_one_description">"Ponovi jednu"</string>
<string name="exo_controls_shuffle_description">"Pusti nasumično"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Паўтарыць усё"</string> <string name="exo_controls_repeat_all_description">"Паўтарыць усё"</string>
<string name="exo_controls_repeat_off_description">"Паўтараць ні"</string> <string name="exo_controls_repeat_off_description">"Паўтараць ні"</string>
<string name="exo_controls_repeat_one_description">"Паўтарыць адзін"</string> <string name="exo_controls_repeat_one_description">"Паўтарыць адзін"</string>
<string name="exo_controls_shuffle_description">"Перамяшаць"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Повтаряне на всички"</string> <string name="exo_controls_repeat_all_description">"Повтаряне на всички"</string>
<string name="exo_controls_repeat_off_description">"Без повтаряне"</string> <string name="exo_controls_repeat_off_description">"Без повтаряне"</string>
<string name="exo_controls_repeat_one_description">"Повтаряне на един елемент"</string> <string name="exo_controls_repeat_one_description">"Повтаряне на един елемент"</string>
<string name="exo_controls_shuffle_description">"Разбъркване"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string> <string name="exo_controls_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string>
<string name="exo_controls_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string> <string name="exo_controls_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string>
<string name="exo_controls_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string> <string name="exo_controls_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string>
<string name="exo_controls_shuffle_description">"অদলবদল"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Ponovite sve"</string> <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_off_description">"Ne ponavljaju"</string>
<string name="exo_controls_repeat_one_description">"Ponovite jedan"</string> <string name="exo_controls_repeat_one_description">"Ponovite jedan"</string>
<string name="exo_controls_shuffle_description">"Izmiješaj"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeteix-ho tot"</string> <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_off_description">"No en repeteixis cap"</string>
<string name="exo_controls_repeat_one_description">"Repeteix-ne un"</string> <string name="exo_controls_repeat_one_description">"Repeteix-ne un"</string>
<string name="exo_controls_shuffle_description">"Reprodueix aleatòriament"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Opakovat vše"</string> <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_off_description">"Neopakovat"</string>
<string name="exo_controls_repeat_one_description">"Opakovat jednu položku"</string> <string name="exo_controls_repeat_one_description">"Opakovat jednu položku"</string>
<string name="exo_controls_shuffle_description">"Náhodně"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Gentag alle"</string> <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_off_description">"Gentag ingen"</string>
<string name="exo_controls_repeat_one_description">"Gentag en"</string> <string name="exo_controls_repeat_one_description">"Gentag en"</string>
<string name="exo_controls_shuffle_description">"Bland"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Alle wiederholen"</string> <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_off_description">"Keinen Titel wiederholen"</string>
<string name="exo_controls_repeat_one_description">"Einen Titel wiederholen"</string> <string name="exo_controls_repeat_one_description">"Einen Titel wiederholen"</string>
<string name="exo_controls_shuffle_description">"Zufallsmix"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Επανάληψη όλων"</string> <string name="exo_controls_repeat_all_description">"Επανάληψη όλων"</string>
<string name="exo_controls_repeat_off_description">"Καμία επανάληψη"</string> <string name="exo_controls_repeat_off_description">"Καμία επανάληψη"</string>
<string name="exo_controls_repeat_one_description">"Επανάληψη ενός στοιχείου"</string> <string name="exo_controls_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
<string name="exo_controls_shuffle_description">"Τυχαία αναπαραγωγή"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string> <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_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string> <string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string> <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_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string> <string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repeat all"</string> <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_off_description">"Repeat none"</string>
<string name="exo_controls_repeat_one_description">"Repeat one"</string> <string name="exo_controls_repeat_one_description">"Repeat one"</string>
<string name="exo_controls_shuffle_description">"Shuffle"</string>
</resources> </resources>
...@@ -25,4 +25,5 @@ ...@@ -25,4 +25,5 @@
<string name="exo_controls_repeat_all_description">"Repetir todo"</string> <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_off_description">"No repetir"</string>
<string name="exo_controls_repeat_one_description">"Repetir uno"</string> <string name="exo_controls_repeat_one_description">"Repetir uno"</string>
<string name="exo_controls_shuffle_description">"Reproducir aleatoriamente"</string>
</resources> </resources>
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