Merge branch 'dev-v2' into release-v2
|
|
@ -11,7 +11,7 @@
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply from: '../constants.gradle'
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
|
|
|
|||
|
|
@ -472,13 +472,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
}
|
||||
if (!imaPlayingAd) {
|
||||
imaPlayingAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPlay();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPlay();
|
||||
}
|
||||
} else if (imaPausedInAd) {
|
||||
imaPausedInAd = false;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onResume();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onResume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -509,8 +509,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
return;
|
||||
}
|
||||
imaPausedInAd = true;
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onPause();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onPause();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -555,8 +555,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
} else if (imaPlayingAd && playbackState == Player.STATE_ENDED) {
|
||||
// IMA is waiting for the ad playback to finish so invoke the callback now.
|
||||
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onEnded();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -569,8 +569,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
@Override
|
||||
public void onPlayerError(ExoPlaybackException error) {
|
||||
if (playingAd) {
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onError();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -630,8 +630,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer,
|
|||
if (adFinished) {
|
||||
// IMA is waiting for the ad playback to finish so invoke the callback now.
|
||||
// Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again.
|
||||
for (VideoAdPlayerCallback callback : adCallbacks) {
|
||||
callback.onEnded();
|
||||
for (int i = 0; i < adCallbacks.size(); i++) {
|
||||
adCallbacks.get(i).onEnded();
|
||||
}
|
||||
}
|
||||
if (!wasPlayingAd && playingAd) {
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima;
|
|||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.ForwardingTimeline;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
||||
/**
|
||||
* A {@link Timeline} for sources that have ads.
|
||||
*/
|
||||
/* package */ final class SinglePeriodAdTimeline extends Timeline {
|
||||
/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline {
|
||||
|
||||
private final Timeline contentTimeline;
|
||||
private final long[] adGroupTimesUs;
|
||||
private final int[] adCounts;
|
||||
private final int[] adsLoadedCounts;
|
||||
|
|
@ -52,9 +52,9 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
|
||||
int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs,
|
||||
long adResumePositionUs) {
|
||||
super(contentTimeline);
|
||||
Assertions.checkState(contentTimeline.getPeriodCount() == 1);
|
||||
Assertions.checkState(contentTimeline.getWindowCount() == 1);
|
||||
this.contentTimeline = contentTimeline;
|
||||
this.adGroupTimesUs = adGroupTimesUs;
|
||||
this.adCounts = adCounts;
|
||||
this.adsLoadedCounts = adsLoadedCounts;
|
||||
|
|
@ -63,34 +63,13 @@ import com.google.android.exoplayer2.util.Assertions;
|
|||
this.adResumePositionUs = adResumePositionUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
contentTimeline.getPeriod(periodIndex, period, setIds);
|
||||
timeline.getPeriod(periodIndex, period, setIds);
|
||||
period.set(period.id, period.uid, period.windowIndex, period.durationUs,
|
||||
period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts,
|
||||
adDurationsUs, adResumePositionUs);
|
||||
return period;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return contentTimeline.getIndexOfPeriod(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession;
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
|
|
|||
|
|
@ -15,20 +15,18 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerWrapper;
|
||||
import com.google.android.exoplayer2.testutil.ActionSchedule;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeMediaSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeRenderer;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline;
|
||||
import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.LinkedList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -43,67 +41,59 @@ public final class ExoPlayerTest extends TestCase {
|
|||
*/
|
||||
private static final int TIMEOUT_MS = 10000;
|
||||
|
||||
private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
|
||||
MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
|
||||
null, null);
|
||||
private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
|
||||
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
|
||||
* error.
|
||||
*/
|
||||
public void testPlayEmptyTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = Timeline.EMPTY;
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
FakeRenderer renderer = new FakeRenderer();
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertEquals(0, renderer.formatReadCount);
|
||||
assertEquals(0, renderer.bufferReadCount);
|
||||
assertFalse(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Object manifest = new Object();
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setManifest(manifest).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
testRunner.assertManifestsEqual(manifest);
|
||||
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes three periods.
|
||||
*/
|
||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertEquals(3, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -111,16 +101,12 @@ public final class ExoPlayerTest extends TestCase {
|
|||
* source.
|
||||
*/
|
||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||
final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT,
|
||||
TEST_AUDIO_FORMAT);
|
||||
|
||||
FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) {
|
||||
final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
|
||||
|
||||
@Override
|
||||
public long getPositionUs() {
|
||||
|
|
@ -143,35 +129,30 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
// Allow playback to end once the final period is playing.
|
||||
return playerWrapper.positionDiscontinuityCount == 2;
|
||||
return videoRenderer.isEnded();
|
||||
}
|
||||
|
||||
};
|
||||
playerWrapper.setup(mediaSource, videoRenderer, audioRenderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(videoRenderer, audioRenderer)
|
||||
.setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertEquals(1, audioRenderer.positionResetCount);
|
||||
assertTrue(videoRenderer.isEnded);
|
||||
assertTrue(audioRenderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
public void testRepreparationGivesFreshSourceInfo() throws Exception {
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
|
||||
// Prepare the player with a source with the first manifest and a non-empty timeline
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
Object firstSourceManifest = new Object();
|
||||
playerWrapper.setup(new FakeMediaSource(timeline, firstSourceManifest, TEST_VIDEO_FORMAT),
|
||||
renderer);
|
||||
playerWrapper.blockUntilSourceInfoRefreshed(TIMEOUT_MS);
|
||||
|
||||
// Prepare the player again with a source and a new manifest, which will never be exposed.
|
||||
MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
|
||||
Builder.VIDEO_FORMAT);
|
||||
final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
|
||||
final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
|
||||
playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) {
|
||||
MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) {
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
super.prepareSource(player, isTopLevelSource, listener);
|
||||
|
|
@ -185,29 +166,49 @@ public final class ExoPlayerTest extends TestCase {
|
|||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare the player again with a third source.
|
||||
queuedSourceInfoCountDownLatch.await();
|
||||
};
|
||||
Object thirdSourceManifest = new Object();
|
||||
playerWrapper.prepare(new FakeMediaSource(timeline, thirdSourceManifest, TEST_VIDEO_FORMAT));
|
||||
completePreparationCountDownLatch.countDown();
|
||||
|
||||
// Wait for playback to complete.
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
|
||||
MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest,
|
||||
Builder.VIDEO_FORMAT);
|
||||
|
||||
// Prepare the player with a source with the first manifest and a non-empty timeline. Prepare
|
||||
// the player again with a source and a new manifest, which will never be exposed. Allow the
|
||||
// test thread to prepare the player with a third source, and block the playback thread until
|
||||
// the test thread's call to prepare() has returned.
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation")
|
||||
.waitForTimelineChanged(timeline)
|
||||
.prepareSource(secondSource)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
queuedSourceInfoCountDownLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
})
|
||||
.prepareSource(thirdSource)
|
||||
.executeRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
completePreparationCountDownLatch.countDown();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPositionDiscontinuityCount(0);
|
||||
// The first source's preparation completed with a non-empty timeline. When the player was
|
||||
// re-prepared with the second source, it immediately exposed an empty timeline, but the source
|
||||
// info refresh from the second source was suppressed as we re-prepared with the third source.
|
||||
playerWrapper.assertSourceInfosEquals(
|
||||
Pair.create(timeline, firstSourceManifest),
|
||||
Pair.create(Timeline.EMPTY, null),
|
||||
Pair.create(timeline, thirdSourceManifest));
|
||||
testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline);
|
||||
testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest);
|
||||
testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT)));
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
}
|
||||
|
||||
public void testRepeatModeChanges() throws Exception {
|
||||
|
|
@ -215,49 +216,22 @@ public final class ExoPlayerTest extends TestCase {
|
|||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000),
|
||||
new TimelineWindowDefinition(true, false, 100000));
|
||||
final int[] actionSchedule = { // 0 -> 1
|
||||
Player.REPEAT_MODE_ONE, // 1 -> 1
|
||||
Player.REPEAT_MODE_OFF, // 1 -> 2
|
||||
Player.REPEAT_MODE_ONE, // 2 -> 2
|
||||
Player.REPEAT_MODE_ALL, // 2 -> 0
|
||||
Player.REPEAT_MODE_ONE, // 0 -> 0
|
||||
-1, // 0 -> 0
|
||||
Player.REPEAT_MODE_OFF, // 0 -> 1
|
||||
-1, // 1 -> 2
|
||||
-1 // 2 -> ended
|
||||
};
|
||||
int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2};
|
||||
final LinkedList<Integer> windowIndices = new LinkedList<>();
|
||||
final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length);
|
||||
ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() {
|
||||
@Override
|
||||
@SuppressWarnings("ResourceType")
|
||||
public void onPositionDiscontinuity() {
|
||||
super.onPositionDiscontinuity();
|
||||
int actionIndex = actionSchedule.length - (int) actionCounter.getCount();
|
||||
if (actionSchedule[actionIndex] != -1) {
|
||||
player.setRepeatMode(actionSchedule[actionIndex]);
|
||||
}
|
||||
windowIndices.add(player.getCurrentWindowIndex());
|
||||
actionCounter.countDown();
|
||||
}
|
||||
};
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
playerWrapper.release();
|
||||
assertTrue("Test playback timed out waiting for action schedule to end.", finished);
|
||||
if (playerWrapper.exception != null) {
|
||||
throw playerWrapper.exception;
|
||||
}
|
||||
assertEquals(expectedWindowIndices.length, windowIndices.size());
|
||||
for (int i = 0; i < expectedWindowIndices.length; i++) {
|
||||
assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue());
|
||||
}
|
||||
assertEquals(9, playerWrapper.positionDiscontinuityCount);
|
||||
FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
|
||||
ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 2 -> 2
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ALL) // 2 -> 0
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 0 -> 0
|
||||
.waitForPositionDiscontinuity() // 0 -> 0
|
||||
.waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 0 -> end
|
||||
.build();
|
||||
ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
|
||||
.setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule)
|
||||
.build().start().blockUntilEnded(TIMEOUT_MS);
|
||||
testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2);
|
||||
testRunner.assertTimelinesEqual(timeline);
|
||||
assertTrue(renderer.isEnded);
|
||||
playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
|||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(4000, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
new byte[] {'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
|
|
@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
|||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
TestUtil.buildTestData(2046, random),
|
||||
new byte[]{'O', 'g', 'g', 'S'},
|
||||
new byte[] {'O', 'g', 'g', 'S'},
|
||||
TestUtil.buildTestData(4000, random)
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
|
|
@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
|||
public void testSkipToNextPageInputShorterThanPeekLength() throws Exception {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
TestUtil.joinByteArrays(
|
||||
new byte[]{'x', 'O', 'g', 'g', 'S'}
|
||||
new byte[] {'x', 'O', 'g', 'g', 'S'}
|
||||
), false);
|
||||
skipToNextPage(extractorInput);
|
||||
assertEquals(1, extractorInput.getPosition());
|
||||
|
|
@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
|
|||
|
||||
public void testSkipToNextPageNoMatch() throws Exception {
|
||||
FakeExtractorInput extractorInput = TestData.createInput(
|
||||
new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false);
|
||||
new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false);
|
||||
try {
|
||||
skipToNextPage(extractorInput);
|
||||
fail();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
|
|
@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase {
|
|||
}
|
||||
}
|
||||
|
||||
public void testAdtsDataOnly() throws Exception {
|
||||
public void testAdtsDataOnly() throws ParserException {
|
||||
data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length);
|
||||
feed();
|
||||
assertSampleCounts(0, 1);
|
||||
adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null);
|
||||
}
|
||||
|
||||
private void feedLimited(int limit) {
|
||||
private void feedLimited(int limit) throws ParserException {
|
||||
maybeStartPacket();
|
||||
data.setLimit(limit);
|
||||
feed();
|
||||
}
|
||||
|
||||
private void feed() {
|
||||
private void feed() throws ParserException {
|
||||
maybeStartPacket();
|
||||
adtsReader.consume(data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase {
|
|||
|
||||
public void testAdvanceToBeforeBuffer() {
|
||||
writeTestData();
|
||||
boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
|
||||
int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false);
|
||||
// Should fail and have no effect.
|
||||
assertFalse(result);
|
||||
assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
|
||||
assertReadTestData();
|
||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
||||
}
|
||||
|
||||
public void testAdvanceToStartOfBuffer() {
|
||||
writeTestData();
|
||||
boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
|
||||
int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false);
|
||||
// Should succeed but have no effect (we're already at the first frame).
|
||||
assertTrue(result);
|
||||
assertEquals(0, skipCount);
|
||||
assertReadTestData();
|
||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
||||
}
|
||||
|
||||
public void testAdvanceToEndOfBuffer() {
|
||||
writeTestData();
|
||||
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
|
||||
// Should succeed and skip to 2nd keyframe.
|
||||
assertTrue(result);
|
||||
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false);
|
||||
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
||||
assertEquals(4, skipCount);
|
||||
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
|
||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
||||
}
|
||||
|
||||
public void testAdvanceToAfterBuffer() {
|
||||
writeTestData();
|
||||
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
|
||||
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false);
|
||||
// Should fail and have no effect.
|
||||
assertFalse(result);
|
||||
assertEquals(SampleQueue.ADVANCE_FAILED, skipCount);
|
||||
assertReadTestData();
|
||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
||||
}
|
||||
|
||||
public void testAdvanceToAfterBufferAllowed() {
|
||||
writeTestData();
|
||||
boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
|
||||
// Should succeed and skip to 2nd keyframe.
|
||||
assertTrue(result);
|
||||
int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true);
|
||||
// Should succeed and skip to 2nd keyframe (the 4th frame).
|
||||
assertEquals(4, skipCount);
|
||||
assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX);
|
||||
assertNoSamplesToRead(TEST_FORMAT_2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Unit test for {@link ShuffleOrder}.
|
||||
*/
|
||||
public final class ShuffleOrderTest extends TestCase {
|
||||
|
||||
public static final long RANDOM_SEED = 1234567890L;
|
||||
|
||||
public void testDefaultShuffleOrder() {
|
||||
assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0);
|
||||
assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1);
|
||||
assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5);
|
||||
for (int initialLength = 0; initialLength < 4; initialLength++) {
|
||||
for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
|
||||
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0);
|
||||
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1);
|
||||
testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5);
|
||||
}
|
||||
}
|
||||
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0);
|
||||
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2);
|
||||
testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4);
|
||||
testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0);
|
||||
}
|
||||
|
||||
public void testUnshuffledShuffleOrder() {
|
||||
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0);
|
||||
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1);
|
||||
assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5);
|
||||
for (int initialLength = 0; initialLength < 4; initialLength++) {
|
||||
for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) {
|
||||
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0);
|
||||
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1);
|
||||
testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5);
|
||||
}
|
||||
}
|
||||
testCloneAndRemove(new UnshuffledShuffleOrder(5), 0);
|
||||
testCloneAndRemove(new UnshuffledShuffleOrder(5), 2);
|
||||
testCloneAndRemove(new UnshuffledShuffleOrder(5), 4);
|
||||
testCloneAndRemove(new UnshuffledShuffleOrder(1), 0);
|
||||
}
|
||||
|
||||
public void testUnshuffledShuffleOrderIsUnshuffled() {
|
||||
ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5);
|
||||
assertEquals(0, shuffleOrder.getFirstIndex());
|
||||
assertEquals(4, shuffleOrder.getLastIndex());
|
||||
for (int i = 0; i < 4; i++) {
|
||||
assertEquals(i + 1, shuffleOrder.getNextIndex(i));
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) {
|
||||
assertEquals(length, shuffleOrder.getLength());
|
||||
if (length == 0) {
|
||||
assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex());
|
||||
assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex());
|
||||
} else {
|
||||
int[] indices = new int[length];
|
||||
indices[0] = shuffleOrder.getFirstIndex();
|
||||
assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0]));
|
||||
for (int i = 1; i < length; i++) {
|
||||
indices[i] = shuffleOrder.getNextIndex(indices[i - 1]);
|
||||
assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i]));
|
||||
for (int j = 0; j < i; j++) {
|
||||
assertTrue(indices[i] != indices[j]);
|
||||
}
|
||||
}
|
||||
assertEquals(indices[length - 1], shuffleOrder.getLastIndex());
|
||||
assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1]));
|
||||
for (int i = 0; i < length; i++) {
|
||||
assertTrue(indices[i] >= 0 && indices[i] < length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) {
|
||||
ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count);
|
||||
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count);
|
||||
// Assert all elements still have the relative same order
|
||||
for (int i = 0; i < shuffleOrder.getLength(); i++) {
|
||||
int expectedNextIndex = shuffleOrder.getNextIndex(i);
|
||||
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
|
||||
expectedNextIndex += count;
|
||||
}
|
||||
int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count);
|
||||
while (newNextIndex >= position && newNextIndex < position + count) {
|
||||
newNextIndex = newOrder.getNextIndex(newNextIndex);
|
||||
}
|
||||
assertEquals(expectedNextIndex, newNextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) {
|
||||
ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position);
|
||||
assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1);
|
||||
// Assert all elements still have the relative same order
|
||||
for (int i = 0; i < shuffleOrder.getLength(); i++) {
|
||||
if (i == position) {
|
||||
continue;
|
||||
}
|
||||
int expectedNextIndex = shuffleOrder.getNextIndex(i);
|
||||
if (expectedNextIndex == position) {
|
||||
expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex);
|
||||
}
|
||||
if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) {
|
||||
expectedNextIndex--;
|
||||
}
|
||||
int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1);
|
||||
assertEquals(expectedNextIndex, newNextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import android.test.MoreAsserts;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
|
|
@ -27,8 +26,14 @@ public final class ParsableBitArrayTest extends TestCase {
|
|||
private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01,
|
||||
(byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99};
|
||||
|
||||
private ParsableBitArray testArray;
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
testArray = new ParsableBitArray(TEST_DATA);
|
||||
}
|
||||
|
||||
public void testReadAllBytes() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
byte[] bytesRead = new byte[TEST_DATA.length];
|
||||
testArray.readBytes(bytesRead, 0, TEST_DATA.length);
|
||||
MoreAsserts.assertEquals(TEST_DATA, bytesRead);
|
||||
|
|
@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testReadBit() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
assertReadBitsToEnd(0, testArray);
|
||||
assertReadBitsToEnd(0);
|
||||
}
|
||||
|
||||
public void testReadBits() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
|
||||
assertEquals(getTestDataBits(5, 0), testArray.readBits(0));
|
||||
assertEquals(getTestDataBits(5, 3), testArray.readBits(3));
|
||||
assertEquals(getTestDataBits(8, 16), testArray.readBits(16));
|
||||
assertEquals(getTestDataBits(24, 3), testArray.readBits(3));
|
||||
|
|
@ -52,67 +56,101 @@ public final class ParsableBitArrayTest extends TestCase {
|
|||
assertEquals(getTestDataBits(50, 14), testArray.readBits(14));
|
||||
}
|
||||
|
||||
public void testReadBitsToByteArray() {
|
||||
byte[] result = new byte[TEST_DATA.length];
|
||||
// Test read within byte boundaries.
|
||||
testArray.readBits(result, 0, 6);
|
||||
assertEquals(TEST_DATA[0] & 0xFC, result[0]);
|
||||
// Test read across byte boundaries.
|
||||
testArray.readBits(result, 0, 8);
|
||||
assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]);
|
||||
// Test reading across multiple bytes.
|
||||
testArray.readBits(result, 1, 50);
|
||||
for (int i = 1; i < 7; i++) {
|
||||
assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)),
|
||||
result[i]);
|
||||
}
|
||||
assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]);
|
||||
assertEquals(0, testArray.bitsLeft());
|
||||
// Test read last buffer byte across input data bytes.
|
||||
testArray.setPosition(31);
|
||||
result[3] = 0;
|
||||
testArray.readBits(result, 3, 3);
|
||||
assertEquals((byte) 0xE0, result[3]);
|
||||
// Test read bits in the middle of a input data byte.
|
||||
result[0] = 0;
|
||||
assertEquals(34, testArray.getPosition());
|
||||
testArray.readBits(result, 0, 3);
|
||||
assertEquals((byte) 0xE0, result[0]);
|
||||
// Test read 0 bits.
|
||||
testArray.setPosition(32);
|
||||
result[1] = 0;
|
||||
testArray.readBits(result, 1, 0);
|
||||
assertEquals(0, result[1]);
|
||||
// Test reading a number of bits divisible by 8.
|
||||
testArray.setPosition(0);
|
||||
testArray.readBits(result, 0, 16);
|
||||
assertEquals(TEST_DATA[0], result[0]);
|
||||
assertEquals(TEST_DATA[1], result[1]);
|
||||
// Test least significant bits are unmodified.
|
||||
result[1] = (byte) 0xFF;
|
||||
testArray.readBits(result, 0, 9);
|
||||
assertEquals(0x5F, result[0]);
|
||||
assertEquals(0x7F, result[1]);
|
||||
}
|
||||
|
||||
public void testRead32BitsByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
assertEquals(getTestDataBits(0, 32), testArray.readBits(32));
|
||||
assertEquals(getTestDataBits(32, 32), testArray.readBits(32));
|
||||
}
|
||||
|
||||
public void testRead32BitsNonByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
assertEquals(getTestDataBits(0, 5), testArray.readBits(5));
|
||||
assertEquals(getTestDataBits(5, 32), testArray.readBits(32));
|
||||
}
|
||||
|
||||
public void testSkipBytes() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.skipBytes(2);
|
||||
assertReadBitsToEnd(16, testArray);
|
||||
assertReadBitsToEnd(16);
|
||||
}
|
||||
|
||||
public void testSkipBitsByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.skipBits(16);
|
||||
assertReadBitsToEnd(16, testArray);
|
||||
assertReadBitsToEnd(16);
|
||||
}
|
||||
|
||||
public void testSkipBitsNonByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.skipBits(5);
|
||||
assertReadBitsToEnd(5, testArray);
|
||||
assertReadBitsToEnd(5);
|
||||
}
|
||||
|
||||
public void testSetPositionByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.setPosition(16);
|
||||
assertReadBitsToEnd(16, testArray);
|
||||
assertReadBitsToEnd(16);
|
||||
}
|
||||
|
||||
public void testSetPositionNonByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.setPosition(5);
|
||||
assertReadBitsToEnd(5, testArray);
|
||||
assertReadBitsToEnd(5);
|
||||
}
|
||||
|
||||
public void testByteAlignFromNonByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.setPosition(11);
|
||||
testArray.byteAlign();
|
||||
assertEquals(2, testArray.getBytePosition());
|
||||
assertEquals(16, testArray.getPosition());
|
||||
assertReadBitsToEnd(16, testArray);
|
||||
assertReadBitsToEnd(16);
|
||||
}
|
||||
|
||||
public void testByteAlignFromByteAligned() {
|
||||
ParsableBitArray testArray = new ParsableBitArray(TEST_DATA);
|
||||
testArray.setPosition(16);
|
||||
testArray.byteAlign(); // Should be a no-op.
|
||||
assertEquals(2, testArray.getBytePosition());
|
||||
assertEquals(16, testArray.getPosition());
|
||||
assertReadBitsToEnd(16, testArray);
|
||||
assertReadBitsToEnd(16);
|
||||
}
|
||||
|
||||
private static void assertReadBitsToEnd(int expectedStartPosition, ParsableBitArray testArray) {
|
||||
private void assertReadBitsToEnd(int expectedStartPosition) {
|
||||
int position = testArray.getPosition();
|
||||
assertEquals(expectedStartPosition, position);
|
||||
for (int i = position; i < TEST_DATA.length * 8; i++) {
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testReadLittleEndianLong() {
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
|
||||
0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, (byte) 0xFF
|
||||
});
|
||||
|
|
@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testReadLittleEndianInt() {
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
|
||||
0x01, 0x00, 0x00, (byte) 0xFF
|
||||
});
|
||||
assertEquals(0xFF000001, byteArray.readLittleEndianInt());
|
||||
|
|
@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testReadLittleEndianUnsignedShort() {
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
|
||||
0x01, (byte) 0xFF, 0x02, (byte) 0xFF
|
||||
});
|
||||
assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort());
|
||||
|
|
@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase {
|
|||
}
|
||||
|
||||
public void testReadLittleEndianShort() {
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[]{
|
||||
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {
|
||||
0x01, (byte) 0xFF, 0x02, (byte) 0xFF
|
||||
});
|
||||
assertEquals((short) 0xFF01, byteArray.readLittleEndianShort());
|
||||
|
|
|
|||
|
|
@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
* {@code positionUs} is beyond it.
|
||||
*
|
||||
* @param positionUs The position in microseconds.
|
||||
* @return The number of samples that were skipped.
|
||||
*/
|
||||
protected void skipSource(long positionUs) {
|
||||
stream.skipData(positionUs - streamOffsetUs);
|
||||
protected int skipSource(long positionUs) {
|
||||
return stream.skipData(positionUs - streamOffsetUs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
|
||||
|
||||
// Build the player and associated objects.
|
||||
player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
|
||||
player = createExoPlayerImpl(renderers, trackSelector, loadControl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -723,6 +723,19 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
|
||||
// Internal methods.
|
||||
|
||||
/**
|
||||
* Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}.
|
||||
*
|
||||
* @param renderers The {@link Renderer}s that will be used by the instance.
|
||||
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
|
||||
* @param loadControl The {@link LoadControl} that will be used by the instance.
|
||||
* @return A new {@link ExoPlayer} instance.
|
||||
*/
|
||||
protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector,
|
||||
LoadControl loadControl) {
|
||||
return new ExoPlayerImpl(renderers, trackSelector, loadControl);
|
||||
}
|
||||
|
||||
private void removeSurfaceCallbacks() {
|
||||
if (textureView != null) {
|
||||
if (textureView.getSurfaceTextureListener() != componentListener) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public final class DecoderCounters {
|
|||
* The number of queued input buffers.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -79,6 +85,7 @@ public final class DecoderCounters {
|
|||
decoderInitCount += other.decoderInitCount;
|
||||
decoderReleaseCount += other.decoderReleaseCount;
|
||||
inputBufferCount += other.inputBufferCount;
|
||||
skippedInputBufferCount += other.skippedInputBufferCount;
|
||||
renderedOutputBufferCount += other.renderedOutputBufferCount;
|
||||
skippedOutputBufferCount += other.skippedOutputBufferCount;
|
||||
droppedOutputBufferCount += other.droppedOutputBufferCount;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.media.MediaDrm;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv;
|
|||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
|
@ -85,7 +86,7 @@ import java.util.Collections;
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) {
|
||||
protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException {
|
||||
if (audioFormat == AUDIO_FORMAT_MP3) {
|
||||
int sampleSize = data.bytesLeft();
|
||||
output.sampleData(data, sampleSize);
|
||||
|
|
|
|||
|
|
@ -816,7 +816,7 @@ import java.util.List;
|
|||
|
||||
private static void parseAudioSampleEntry(ParsableByteArray parent, int atomType, int position,
|
||||
int size, int trackId, String language, boolean isQuickTime, DrmInitData drmInitData,
|
||||
StsdData out, int entryIndex) {
|
||||
StsdData out, int entryIndex) throws ParserException {
|
||||
parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE);
|
||||
|
||||
int quickTimeSoundDescriptionVersion = 0;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import android.util.Log;
|
|||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.DummyTrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
|
|
@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) {
|
||||
public void consume(ParsableByteArray data) throws ParserException {
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SAMPLE:
|
||||
|
|
@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader {
|
|||
/**
|
||||
* Parses the sample header.
|
||||
*/
|
||||
private void parseAdtsHeader() {
|
||||
private void parseAdtsHeader() throws ParserException {
|
||||
adtsScratch.setPosition(0);
|
||||
|
||||
if (!hasOutputFormat) {
|
||||
|
|
|
|||
|
|
@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact
|
|||
case TsExtractor.TS_STREAM_TYPE_MPA:
|
||||
case TsExtractor.TS_STREAM_TYPE_MPA_LSF:
|
||||
return new PesReader(new MpegAudioReader(esInfo.language));
|
||||
case TsExtractor.TS_STREAM_TYPE_AAC:
|
||||
case TsExtractor.TS_STREAM_TYPE_AAC_ADTS:
|
||||
return isSet(FLAG_IGNORE_AAC_STREAM)
|
||||
? null : new PesReader(new AdtsReader(false, esInfo.language));
|
||||
case TsExtractor.TS_STREAM_TYPE_AAC_LATM:
|
||||
return isSet(FLAG_IGNORE_AAC_STREAM)
|
||||
? null : new PesReader(new LatmReader(esInfo.language));
|
||||
case TsExtractor.TS_STREAM_TYPE_AC3:
|
||||
case TsExtractor.TS_STREAM_TYPE_E_AC3:
|
||||
return new PesReader(new Ac3Reader(esInfo.language));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -50,8 +51,9 @@ public interface ElementaryStreamReader {
|
|||
* Consumes (possibly partial) data from the current packet.
|
||||
*
|
||||
* @param data The data to consume.
|
||||
* @throws ParserException If the data could not be parsed.
|
||||
*/
|
||||
void consume(ParsableByteArray data);
|
||||
void consume(ParsableByteArray data) throws ParserException;
|
||||
|
||||
/**
|
||||
* Called when a packet ends.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,307 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Parses and extracts samples from an AAC/LATM elementary stream.
|
||||
*/
|
||||
public final class LatmReader implements ElementaryStreamReader {
|
||||
|
||||
private static final int STATE_FINDING_SYNC_1 = 0;
|
||||
private static final int STATE_FINDING_SYNC_2 = 1;
|
||||
private static final int STATE_READING_HEADER = 2;
|
||||
private static final int STATE_READING_SAMPLE = 3;
|
||||
|
||||
private static final int INITIAL_BUFFER_SIZE = 1024;
|
||||
private static final int SYNC_BYTE_FIRST = 0x56;
|
||||
private static final int SYNC_BYTE_SECOND = 0xE0;
|
||||
|
||||
private final String language;
|
||||
private final ParsableByteArray sampleDataBuffer;
|
||||
private final ParsableBitArray sampleBitArray;
|
||||
|
||||
// Track output info.
|
||||
private TrackOutput output;
|
||||
private Format format;
|
||||
private String formatId;
|
||||
|
||||
// Parser state info.
|
||||
private int state;
|
||||
private int bytesRead;
|
||||
private int sampleSize;
|
||||
private int secondHeaderByte;
|
||||
private long timeUs;
|
||||
|
||||
// Container data.
|
||||
private boolean streamMuxRead;
|
||||
private int audioMuxVersion;
|
||||
private int audioMuxVersionA;
|
||||
private int numSubframes;
|
||||
private int frameLengthType;
|
||||
private boolean otherDataPresent;
|
||||
private long otherDataLenBits;
|
||||
private int sampleRateHz;
|
||||
private long sampleDurationUs;
|
||||
private int channelCount;
|
||||
|
||||
/**
|
||||
* @param language Track language.
|
||||
*/
|
||||
public LatmReader(@Nullable String language) {
|
||||
this.language = language;
|
||||
sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE);
|
||||
sampleBitArray = new ParsableBitArray(sampleDataBuffer.data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek() {
|
||||
state = STATE_FINDING_SYNC_1;
|
||||
streamMuxRead = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
idGenerator.generateNewId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO);
|
||||
formatId = idGenerator.getFormatId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) {
|
||||
timeUs = pesTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume(ParsableByteArray data) throws ParserException {
|
||||
int bytesToRead;
|
||||
while (data.bytesLeft() > 0) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_SYNC_1:
|
||||
if (data.readUnsignedByte() == SYNC_BYTE_FIRST) {
|
||||
state = STATE_FINDING_SYNC_2;
|
||||
}
|
||||
break;
|
||||
case STATE_FINDING_SYNC_2:
|
||||
int secondByte = data.readUnsignedByte();
|
||||
if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) {
|
||||
secondHeaderByte = secondByte;
|
||||
state = STATE_READING_HEADER;
|
||||
} else if (secondByte != SYNC_BYTE_FIRST) {
|
||||
state = STATE_FINDING_SYNC_1;
|
||||
}
|
||||
break;
|
||||
case STATE_READING_HEADER:
|
||||
sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte();
|
||||
if (sampleSize > sampleDataBuffer.data.length) {
|
||||
resetBufferForSize(sampleSize);
|
||||
}
|
||||
bytesRead = 0;
|
||||
state = STATE_READING_SAMPLE;
|
||||
break;
|
||||
case STATE_READING_SAMPLE:
|
||||
bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
|
||||
data.readBytes(sampleBitArray.data, bytesRead, bytesToRead);
|
||||
bytesRead += bytesToRead;
|
||||
if (bytesRead == sampleSize) {
|
||||
sampleBitArray.setPosition(0);
|
||||
parseAudioMuxElement(sampleBitArray);
|
||||
state = STATE_FINDING_SYNC_1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void packetFinished() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41.
|
||||
*
|
||||
* @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes.
|
||||
*/
|
||||
private void parseAudioMuxElement(ParsableBitArray data) throws ParserException {
|
||||
boolean useSameStreamMux = data.readBit();
|
||||
if (!useSameStreamMux) {
|
||||
streamMuxRead = true;
|
||||
parseStreamMuxConfig(data);
|
||||
} else if (!streamMuxRead) {
|
||||
return; // Parsing cannot continue without StreamMuxConfig information.
|
||||
}
|
||||
|
||||
if (audioMuxVersionA == 0) {
|
||||
if (numSubframes != 0) {
|
||||
throw new ParserException();
|
||||
}
|
||||
int muxSlotLengthBytes = parsePayloadLengthInfo(data);
|
||||
parsePayloadMux(data, muxSlotLengthBytes);
|
||||
if (otherDataPresent) {
|
||||
data.skipBits((int) otherDataLenBits);
|
||||
}
|
||||
} else {
|
||||
throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42.
|
||||
*/
|
||||
private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException {
|
||||
audioMuxVersion = data.readBits(1);
|
||||
audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
|
||||
if (audioMuxVersionA == 0) {
|
||||
if (audioMuxVersion == 1) {
|
||||
latmGetValue(data); // Skip taraBufferFullness.
|
||||
}
|
||||
if (!data.readBit()) {
|
||||
throw new ParserException();
|
||||
}
|
||||
numSubframes = data.readBits(6);
|
||||
int numProgram = data.readBits(4);
|
||||
int numLayer = data.readBits(3);
|
||||
if (numProgram != 0 || numLayer != 0) {
|
||||
throw new ParserException();
|
||||
}
|
||||
if (audioMuxVersion == 0) {
|
||||
int startPosition = data.getPosition();
|
||||
int readBits = parseAudioSpecificConfig(data);
|
||||
data.setPosition(startPosition);
|
||||
byte[] initData = new byte[(readBits + 7) / 8];
|
||||
data.readBits(initData, 0, readBits);
|
||||
Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz,
|
||||
Collections.singletonList(initData), null, 0, language);
|
||||
if (!format.equals(this.format)) {
|
||||
this.format = format;
|
||||
sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate;
|
||||
output.format(format);
|
||||
}
|
||||
} else {
|
||||
int ascLen = (int) latmGetValue(data);
|
||||
int bitsRead = parseAudioSpecificConfig(data);
|
||||
data.skipBits(ascLen - bitsRead); // fillBits.
|
||||
}
|
||||
parseFrameLength(data);
|
||||
otherDataPresent = data.readBit();
|
||||
otherDataLenBits = 0;
|
||||
if (otherDataPresent) {
|
||||
if (audioMuxVersion == 1) {
|
||||
otherDataLenBits = latmGetValue(data);
|
||||
} else {
|
||||
boolean otherDataLenEsc;
|
||||
do {
|
||||
otherDataLenEsc = data.readBit();
|
||||
otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8);
|
||||
} while (otherDataLenEsc);
|
||||
}
|
||||
}
|
||||
boolean crcCheckPresent = data.readBit();
|
||||
if (crcCheckPresent) {
|
||||
data.skipBits(8); // crcCheckSum.
|
||||
}
|
||||
} else {
|
||||
throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009.
|
||||
}
|
||||
}
|
||||
|
||||
private void parseFrameLength(ParsableBitArray data) {
|
||||
frameLengthType = data.readBits(3);
|
||||
switch (frameLengthType) {
|
||||
case 0:
|
||||
data.skipBits(8); // latmBufferFullness.
|
||||
break;
|
||||
case 1:
|
||||
data.skipBits(9); // frameLength.
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
data.skipBits(6); // CELPframeLengthTableIndex.
|
||||
break;
|
||||
case 6:
|
||||
case 7:
|
||||
data.skipBits(1); // HVXCframeLengthTableIndex.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException {
|
||||
int bitsLeft = data.bitsLeft();
|
||||
Pair<Integer, Integer> config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true);
|
||||
sampleRateHz = config.first;
|
||||
channelCount = config.second;
|
||||
return bitsLeft - data.bitsLeft();
|
||||
}
|
||||
|
||||
private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException {
|
||||
int muxSlotLengthBytes = 0;
|
||||
// Assuming single program and single layer.
|
||||
if (frameLengthType == 0) {
|
||||
int tmp;
|
||||
do {
|
||||
tmp = data.readBits(8);
|
||||
muxSlotLengthBytes += tmp;
|
||||
} while (tmp == 255);
|
||||
return muxSlotLengthBytes;
|
||||
} else {
|
||||
throw new ParserException();
|
||||
}
|
||||
}
|
||||
|
||||
private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) {
|
||||
// The start of sample data in
|
||||
int bitPosition = data.getPosition();
|
||||
if ((bitPosition & 0x07) == 0) {
|
||||
// Sample data is byte-aligned. We can output it directly.
|
||||
sampleDataBuffer.setPosition(bitPosition >> 3);
|
||||
} else {
|
||||
// Sample data is not byte-aligned and we need align it ourselves before outputting.
|
||||
// Byte alignment is needed because LATM framing is not supported by MediaCodec.
|
||||
data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8);
|
||||
sampleDataBuffer.setPosition(0);
|
||||
}
|
||||
output.sampleData(sampleDataBuffer, muxLengthBytes);
|
||||
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null);
|
||||
timeUs += sampleDurationUs;
|
||||
}
|
||||
|
||||
private void resetBufferForSize(int newSize) {
|
||||
sampleDataBuffer.reset(newSize);
|
||||
sampleBitArray.reset(sampleDataBuffer.data);
|
||||
}
|
||||
|
||||
private static long latmGetValue(ParsableBitArray data) {
|
||||
int bytesForValue = data.readBits(2);
|
||||
return data.readBits((bytesForValue + 1) * 8);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader {
|
|||
}
|
||||
|
||||
@Override
|
||||
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) {
|
||||
public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator)
|
||||
throws ParserException {
|
||||
if (payloadUnitStartIndicator) {
|
||||
switch (state) {
|
||||
case STATE_FINDING_HEADER:
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster;
|
|||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Facilitates the extraction of data from the MPEG-2 TS container format.
|
||||
* Facilitates the extraction of data from the MPEG-2 PS container format.
|
||||
*/
|
||||
public final class PsExtractor implements Extractor {
|
||||
|
||||
|
|
@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor {
|
|||
* Consumes the payload of a PS packet.
|
||||
*
|
||||
* @param data The PES packet. The position will be set to the start of the payload.
|
||||
* @throws ParserException If the payload could not be parsed.
|
||||
*/
|
||||
public void consume(ParsableByteArray data) {
|
||||
public void consume(ParsableByteArray data) throws ParserException {
|
||||
data.readBytes(pesScratch.data, 0, 3);
|
||||
pesScratch.setPosition(0);
|
||||
parseHeader();
|
||||
|
|
|
|||
|
|
@ -84,7 +84,8 @@ public final class TsExtractor implements Extractor {
|
|||
|
||||
public static final int TS_STREAM_TYPE_MPA = 0x03;
|
||||
public static final int TS_STREAM_TYPE_MPA_LSF = 0x04;
|
||||
public static final int TS_STREAM_TYPE_AAC = 0x0F;
|
||||
public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F;
|
||||
public static final int TS_STREAM_TYPE_AAC_LATM = 0x11;
|
||||
public static final int TS_STREAM_TYPE_AC3 = 0x81;
|
||||
public static final int TS_STREAM_TYPE_DTS = 0x8A;
|
||||
public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -196,7 +197,8 @@ public interface TsPayloadReader {
|
|||
*
|
||||
* @param data The TS packet. The position will be set to the start of the payload.
|
||||
* @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet.
|
||||
* @throws ParserException If the payload could not be parsed.
|
||||
*/
|
||||
void consume(ParsableByteArray data, boolean payloadUnitStartIndicator);
|
||||
void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||
while (feedInputBuffer()) {}
|
||||
TraceUtil.endSection();
|
||||
} else {
|
||||
skipSource(positionUs);
|
||||
decoderCounters.skippedInputBufferCount += skipSource(positionUs);
|
||||
// We need to read any format changes despite not having a codec so that drmSession can be
|
||||
// updated, and so that we have the most recent format should the codec be initialized. We may
|
||||
// also reach the end of the stream. Note that readSource will not read a sample into a
|
||||
|
|
|
|||
|
|
@ -286,8 +286,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
stream.skipData(startUs + positionUs);
|
||||
public int skipData(long positionUs) {
|
||||
return stream.skipData(startUs + positionUs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source;
|
|||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Player.RepeatMode;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
|
|
@ -128,9 +127,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
|||
/**
|
||||
* Provides a clipped view of a specified timeline.
|
||||
*/
|
||||
private static final class ClippingTimeline extends Timeline {
|
||||
private static final class ClippingTimeline extends ForwardingTimeline {
|
||||
|
||||
private final Timeline timeline;
|
||||
private final long startUs;
|
||||
private final long endUs;
|
||||
|
||||
|
|
@ -143,6 +141,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
|||
* of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.
|
||||
*/
|
||||
public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
|
||||
super(timeline);
|
||||
Assertions.checkArgument(timeline.getWindowCount() == 1);
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
Window window = timeline.getWindow(0, new Window(), false);
|
||||
|
|
@ -155,26 +154,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
|||
}
|
||||
Period period = timeline.getPeriod(0, new Period());
|
||||
Assertions.checkArgument(period.getPositionInWindowUs() == 0);
|
||||
this.timeline = timeline;
|
||||
this.startUs = startUs;
|
||||
this.endUs = resolvedEndUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
|
||||
return timeline.getNextWindowIndex(windowIndex, repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) {
|
||||
return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
|
|
@ -196,11 +179,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
|||
return window;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
period = timeline.getPeriod(0, period, setIds);
|
||||
|
|
@ -208,11 +186,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
|||
return period;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return timeline.getIndexOfPeriod(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
|
|||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||
mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||
mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
|
|||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) {
|
||||
mediaSourceHolder.mediaSource.releaseSource();
|
||||
for (int i = 0; i < mediaSourceHolders.size(); i++) {
|
||||
mediaSourceHolders.get(i).mediaSource.releaseSource();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
// Do nothing.
|
||||
public int skipData(long positionUs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ import java.util.Arrays;
|
|||
// sample queue, or if we haven't read anything from the queue since the previous seek
|
||||
// (this case is common for sparse tracks such as metadata tracks). In all other cases a
|
||||
// seek is required.
|
||||
seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
|
||||
seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
|
||||
&& sampleQueue.getReadIndex() != 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -371,12 +371,13 @@ import java.util.Arrays;
|
|||
lastSeekPositionUs);
|
||||
}
|
||||
|
||||
/* package */ void skipData(int track, long positionUs) {
|
||||
/* package */ int skipData(int track, long positionUs) {
|
||||
SampleQueue sampleQueue = sampleQueues[track];
|
||||
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
|
||||
sampleQueue.advanceToEnd();
|
||||
return sampleQueue.advanceToEnd();
|
||||
} else {
|
||||
sampleQueue.advanceTo(positionUs, true, true);
|
||||
int skipCount = sampleQueue.advanceTo(positionUs, true, true);
|
||||
return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +559,8 @@ import java.util.Arrays;
|
|||
for (int i = 0; i < trackCount; i++) {
|
||||
SampleQueue sampleQueue = sampleQueues[i];
|
||||
sampleQueue.rewind();
|
||||
boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
|
||||
boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)
|
||||
!= SampleQueue.ADVANCE_FAILED;
|
||||
// If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue
|
||||
// is successful. We ignore whether seeks within non-AV queues are successful in this case, as
|
||||
// they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
|
||||
|
|
@ -632,8 +634,8 @@ import java.util.Arrays;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
ExtractorMediaPeriod.this.skipData(track, positionUs);
|
||||
public int skipData(long positionUs) {
|
||||
return ExtractorMediaPeriod.this.skipData(track, positionUs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
|
||||
/**
|
||||
* An overridable {@link Timeline} implementation forwarding all methods to another timeline.
|
||||
*/
|
||||
public abstract class ForwardingTimeline extends Timeline {
|
||||
|
||||
protected final Timeline timeline;
|
||||
|
||||
public ForwardingTimeline(Timeline timeline) {
|
||||
this.timeline = timeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return timeline.getWindowCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
|
||||
return timeline.getNextWindowIndex(windowIndex, repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
|
||||
return timeline.getPreviousWindowIndex(windowIndex, repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
return timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return timeline.getPeriodCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
return timeline.getPeriod(periodIndex, period, setIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return timeline.getIndexOfPeriod(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -160,53 +160,25 @@ public final class LoopingMediaSource implements MediaSource {
|
|||
|
||||
}
|
||||
|
||||
private static final class InfinitelyLoopingTimeline extends Timeline {
|
||||
private static final class InfinitelyLoopingTimeline extends ForwardingTimeline {
|
||||
|
||||
private final Timeline childTimeline;
|
||||
|
||||
public InfinitelyLoopingTimeline(Timeline childTimeline) {
|
||||
this.childTimeline = childTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getWindowCount() {
|
||||
return childTimeline.getWindowCount();
|
||||
public InfinitelyLoopingTimeline(Timeline timeline) {
|
||||
super(timeline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
|
||||
int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode);
|
||||
int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode);
|
||||
return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) {
|
||||
int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode);
|
||||
int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode);
|
||||
return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1
|
||||
: childPreviousWindowIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return childTimeline.getPeriodCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
return childTimeline.getPeriod(periodIndex, period, setIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
return childTimeline.getIndexOfPeriod(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -253,32 +253,35 @@ import com.google.android.exoplayer2.util.Util;
|
|||
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
|
||||
* end of the queue, by advancing the read position to the last sample (or keyframe) in the
|
||||
* queue.
|
||||
* @return Whether the operation was a success. A successful advance is one in which the read
|
||||
* position was unchanged or advanced, and is now at a sample meeting the specified criteria.
|
||||
* @return The number of samples that were skipped if the operation was successful, which may be
|
||||
* equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A
|
||||
* successful advance is one in which the read position was unchanged or advanced, and is now
|
||||
* at a sample meeting the specified criteria.
|
||||
*/
|
||||
public synchronized boolean advanceTo(long timeUs, boolean toKeyframe,
|
||||
public synchronized int advanceTo(long timeUs, boolean toKeyframe,
|
||||
boolean allowTimeBeyondBuffer) {
|
||||
int relativeReadIndex = getRelativeIndex(readPosition);
|
||||
if (!hasNextSample() || timeUs < timesUs[relativeReadIndex]
|
||||
|| (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) {
|
||||
return false;
|
||||
return SampleQueue.ADVANCE_FAILED;
|
||||
}
|
||||
int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe);
|
||||
if (offset == -1) {
|
||||
return false;
|
||||
return SampleQueue.ADVANCE_FAILED;
|
||||
}
|
||||
readPosition += offset;
|
||||
return true;
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the read position to the end of the queue.
|
||||
*
|
||||
* @return The number of samples that were skipped.
|
||||
*/
|
||||
public synchronized void advanceToEnd() {
|
||||
if (!hasNextSample()) {
|
||||
return;
|
||||
}
|
||||
public synchronized int advanceToEnd() {
|
||||
int skipCount = length - readPosition;
|
||||
readPosition = length;
|
||||
return skipCount;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ public final class SampleQueue implements TrackOutput {
|
|||
|
||||
}
|
||||
|
||||
public static final int ADVANCE_FAILED = -1;
|
||||
|
||||
private static final int INITIAL_SCRATCH_SIZE = 32;
|
||||
|
||||
private final Allocator allocator;
|
||||
|
|
@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput {
|
|||
|
||||
/**
|
||||
* Advances the read position to the end of the queue.
|
||||
*
|
||||
* @return The number of samples that were skipped.
|
||||
*/
|
||||
public void advanceToEnd() {
|
||||
metadataQueue.advanceToEnd();
|
||||
public int advanceToEnd() {
|
||||
return metadataQueue.advanceToEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput {
|
|||
* time, rather than to any sample before or at that time.
|
||||
* @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the
|
||||
* end of the queue, by advancing the read position to the last sample (or keyframe).
|
||||
* @return Whether the operation was a success. A successful advance is one in which the read
|
||||
* position was unchanged or advanced, and is now at a sample meeting the specified criteria.
|
||||
* @return The number of samples that were skipped if the operation was successful, which may be
|
||||
* equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful
|
||||
* advance is one in which the read position was unchanged or advanced, and is now at a sample
|
||||
* meeting the specified criteria.
|
||||
*/
|
||||
public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
|
||||
public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) {
|
||||
return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ public interface SampleStream {
|
|||
* {@code positionUs} is beyond it.
|
||||
*
|
||||
* @param positionUs The specified time.
|
||||
* @return The number of samples that were skipped.
|
||||
*/
|
||||
void skipData(long positionUs);
|
||||
int skipData(long positionUs);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Shuffled order of indices.
|
||||
*/
|
||||
public interface ShuffleOrder {
|
||||
|
||||
/**
|
||||
* The default {@link ShuffleOrder} implementation for random shuffle order.
|
||||
*/
|
||||
class DefaultShuffleOrder implements ShuffleOrder {
|
||||
|
||||
private final Random random;
|
||||
private final int[] shuffled;
|
||||
private final int[] indexInShuffled;
|
||||
|
||||
/**
|
||||
* Creates an instance with a specified length.
|
||||
*
|
||||
* @param length The length of the shuffle order.
|
||||
*/
|
||||
public DefaultShuffleOrder(int length) {
|
||||
this(length, new Random());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance with a specified length and the specified random seed. Shuffle orders of
|
||||
* the same length initialized with the same random seed are guaranteed to be equal.
|
||||
*
|
||||
* @param length The length of the shuffle order.
|
||||
* @param randomSeed A random seed.
|
||||
*/
|
||||
public DefaultShuffleOrder(int length, long randomSeed) {
|
||||
this(length, new Random(randomSeed));
|
||||
}
|
||||
|
||||
private DefaultShuffleOrder(int length, Random random) {
|
||||
this(createShuffledList(length, random), random);
|
||||
}
|
||||
|
||||
private DefaultShuffleOrder(int[] shuffled, Random random) {
|
||||
this.shuffled = shuffled;
|
||||
this.random = random;
|
||||
this.indexInShuffled = new int[shuffled.length];
|
||||
for (int i = 0; i < shuffled.length; i++) {
|
||||
indexInShuffled[shuffled[i]] = i;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLength() {
|
||||
return shuffled.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextIndex(int index) {
|
||||
int shuffledIndex = indexInShuffled[index];
|
||||
return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousIndex(int index) {
|
||||
int shuffledIndex = indexInShuffled[index];
|
||||
return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastIndex() {
|
||||
return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstIndex() {
|
||||
return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
|
||||
int[] insertionPoints = new int[insertionCount];
|
||||
int[] insertionValues = new int[insertionCount];
|
||||
for (int i = 0; i < insertionCount; i++) {
|
||||
insertionPoints[i] = random.nextInt(shuffled.length + 1);
|
||||
int swapIndex = random.nextInt(i + 1);
|
||||
insertionValues[i] = insertionValues[swapIndex];
|
||||
insertionValues[swapIndex] = i + insertionIndex;
|
||||
}
|
||||
Arrays.sort(insertionPoints);
|
||||
int[] newShuffled = new int[shuffled.length + insertionCount];
|
||||
int indexInOldShuffled = 0;
|
||||
int indexInInsertionList = 0;
|
||||
for (int i = 0; i < shuffled.length + insertionCount; i++) {
|
||||
if (indexInInsertionList < insertionCount
|
||||
&& indexInOldShuffled == insertionPoints[indexInInsertionList]) {
|
||||
newShuffled[i] = insertionValues[indexInInsertionList++];
|
||||
} else {
|
||||
newShuffled[i] = shuffled[indexInOldShuffled++];
|
||||
if (newShuffled[i] >= insertionIndex) {
|
||||
newShuffled[i] += insertionCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShuffleOrder cloneAndRemove(int removalIndex) {
|
||||
int[] newShuffled = new int[shuffled.length - 1];
|
||||
boolean foundRemovedElement = false;
|
||||
for (int i = 0; i < shuffled.length; i++) {
|
||||
if (shuffled[i] == removalIndex) {
|
||||
foundRemovedElement = true;
|
||||
} else {
|
||||
newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex
|
||||
? shuffled[i] - 1 : shuffled[i];
|
||||
}
|
||||
}
|
||||
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
|
||||
}
|
||||
|
||||
private static int[] createShuffledList(int length, Random random) {
|
||||
int[] shuffled = new int[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
int swapIndex = random.nextInt(i + 1);
|
||||
shuffled[i] = shuffled[swapIndex];
|
||||
shuffled[swapIndex] = i;
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ShuffleOrder} implementation which does not shuffle.
|
||||
*/
|
||||
final class UnshuffledShuffleOrder implements ShuffleOrder {
|
||||
|
||||
private final int length;
|
||||
|
||||
/**
|
||||
* Creates an instance with a specified length.
|
||||
*
|
||||
* @param length The length of the shuffle order.
|
||||
*/
|
||||
public UnshuffledShuffleOrder(int length) {
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNextIndex(int index) {
|
||||
return ++index < length ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviousIndex(int index) {
|
||||
return --index >= 0 ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastIndex() {
|
||||
return length > 0 ? length - 1 : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFirstIndex() {
|
||||
return length > 0 ? 0 : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) {
|
||||
return new UnshuffledShuffleOrder(length + insertionCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShuffleOrder cloneAndRemove(int removalIndex) {
|
||||
return new UnshuffledShuffleOrder(length - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns length of shuffle order.
|
||||
*/
|
||||
int getLength();
|
||||
|
||||
/**
|
||||
* Returns the next index in the shuffle order.
|
||||
*
|
||||
* @param index An index.
|
||||
* @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last
|
||||
* element.
|
||||
*/
|
||||
int getNextIndex(int index);
|
||||
|
||||
/**
|
||||
* Returns the previous index in the shuffle order.
|
||||
*
|
||||
* @param index An index.
|
||||
* @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first
|
||||
* element.
|
||||
*/
|
||||
int getPreviousIndex(int index);
|
||||
|
||||
/**
|
||||
* Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
|
||||
* empty.
|
||||
*/
|
||||
int getLastIndex();
|
||||
|
||||
/**
|
||||
* Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is
|
||||
* empty.
|
||||
*/
|
||||
int getFirstIndex();
|
||||
|
||||
/**
|
||||
* Return a copy of the shuffle order with newly inserted elements.
|
||||
*
|
||||
* @param insertionIndex The index in the unshuffled order at which elements are inserted.
|
||||
* @param insertionCount The number of elements inserted at {@code insertionIndex}.
|
||||
* @return A copy of this {@link ShuffleOrder} with newly inserted elements.
|
||||
*/
|
||||
ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
|
||||
|
||||
/**
|
||||
* Return a copy of the shuffle order with one element removed.
|
||||
*
|
||||
* @param removalIndex The index of the element in the unshuffled order which is to be removed.
|
||||
* @return A copy of this {@link ShuffleOrder} without the removed element.
|
||||
*/
|
||||
ShuffleOrder cloneAndRemove(int removalIndex);
|
||||
|
||||
}
|
||||
|
|
@ -235,10 +235,12 @@ import java.util.Arrays;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
if (positionUs > 0) {
|
||||
public int skipData(long positionUs) {
|
||||
if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {
|
||||
streamState = STREAM_STATE_END_OF_STREAM;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
|||
* @return An estimate of the absolute position in microseconds up to which data is buffered, or
|
||||
* {@link C#TIME_END_OF_SOURCE} if the track is fully buffered.
|
||||
*/
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
if (loadingFinished) {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
|
|
@ -185,8 +186,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
|||
public void seekToUs(long positionUs) {
|
||||
lastSeekPositionUs = positionUs;
|
||||
// If we're not pending a reset, see if we can seek within the primary sample queue.
|
||||
boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true,
|
||||
positionUs < getNextLoadPositionUs());
|
||||
boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true,
|
||||
positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED);
|
||||
if (seekInsideBuffer) {
|
||||
// We succeeded. Discard samples and corresponding chunks prior to the seek position.
|
||||
discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
|
||||
|
|
@ -266,13 +267,19 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
public int skipData(long positionUs) {
|
||||
int skipCount;
|
||||
if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) {
|
||||
primarySampleQueue.advanceToEnd();
|
||||
skipCount = primarySampleQueue.advanceToEnd();
|
||||
} else {
|
||||
primarySampleQueue.advanceTo(positionUs, true, true);
|
||||
skipCount = primarySampleQueue.advanceTo(positionUs, true, true);
|
||||
if (skipCount == SampleQueue.ADVANCE_FAILED) {
|
||||
skipCount = 0;
|
||||
}
|
||||
}
|
||||
primarySampleQueue.discardToRead();
|
||||
return skipCount;
|
||||
}
|
||||
|
||||
// Loader.Callback implementation.
|
||||
|
|
@ -470,11 +477,12 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
public int skipData(long positionUs) {
|
||||
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
|
||||
sampleQueue.advanceToEnd();
|
||||
return sampleQueue.advanceToEnd();
|
||||
} else {
|
||||
sampleQueue.advanceTo(positionUs, true, true);
|
||||
int skipCount = sampleQueue.advanceTo(positionUs, true, true);
|
||||
return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -811,43 +811,43 @@ public final class Cea708Decoder extends CeaDecoder {
|
|||
private static final int PEN_OFFSET_NORMAL = 1;
|
||||
|
||||
// The window style properties are specified in the CEA-708 specification.
|
||||
private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{
|
||||
private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] {
|
||||
JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
|
||||
JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
|
||||
JUSTIFICATION_LEFT
|
||||
};
|
||||
private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{
|
||||
private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] {
|
||||
DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
|
||||
DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
|
||||
DIRECTION_TOP_TO_BOTTOM
|
||||
};
|
||||
private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{
|
||||
private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] {
|
||||
DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
|
||||
DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
|
||||
DIRECTION_RIGHT_TO_LEFT
|
||||
};
|
||||
private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{
|
||||
private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] {
|
||||
false, false, false, true, true, true, false
|
||||
};
|
||||
private static final int[] WINDOW_STYLE_FILL = new int[]{
|
||||
private static final int[] WINDOW_STYLE_FILL = new int[] {
|
||||
COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
|
||||
COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
|
||||
};
|
||||
|
||||
// The pen style properties are specified in the CEA-708 specification.
|
||||
private static final int[] PEN_STYLE_FONT_STYLE = new int[]{
|
||||
private static final int[] PEN_STYLE_FONT_STYLE = new int[] {
|
||||
PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
|
||||
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
|
||||
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
|
||||
PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
|
||||
PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
|
||||
};
|
||||
private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{
|
||||
private static final int[] PEN_STYLE_EDGE_TYPE = new int[] {
|
||||
BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
|
||||
BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
|
||||
BORDER_AND_EDGE_TYPE_UNIFORM
|
||||
};
|
||||
private static final int[] PEN_STYLE_BACKGROUND = new int[]{
|
||||
private static final int[] PEN_STYLE_BACKGROUND = new int[] {
|
||||
COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
|
||||
COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ import java.io.OutputStream;
|
|||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import javax.crypto.Cipher;
|
||||
|
|
@ -176,14 +176,14 @@ import javax.crypto.spec.SecretKeySpec;
|
|||
|
||||
/** Removes empty {@link CachedContent} instances from index. */
|
||||
public void removeEmpty() {
|
||||
LinkedList<String> cachedContentToBeRemoved = new LinkedList<>();
|
||||
ArrayList<String> cachedContentToBeRemoved = new ArrayList<>();
|
||||
for (CachedContent cachedContent : keyToContent.values()) {
|
||||
if (cachedContent.isEmpty()) {
|
||||
cachedContentToBeRemoved.add(cachedContent.key);
|
||||
}
|
||||
}
|
||||
for (String key : cachedContentToBeRemoved) {
|
||||
removeEmpty(key);
|
||||
for (int i = 0; i < cachedContentToBeRemoved.size(); i++) {
|
||||
removeEmpty(cachedContentToBeRemoved.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import java.io.File;
|
|||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
|
@ -308,7 +307,7 @@ public final class SimpleCache implements Cache {
|
|||
* no longer exist.
|
||||
*/
|
||||
private void removeStaleSpansAndCachedContents() throws CacheException {
|
||||
LinkedList<CacheSpan> spansToBeRemoved = new LinkedList<>();
|
||||
ArrayList<CacheSpan> spansToBeRemoved = new ArrayList<>();
|
||||
for (CachedContent cachedContent : index.getAll()) {
|
||||
for (CacheSpan span : cachedContent.getSpans()) {
|
||||
if (!span.file.exists()) {
|
||||
|
|
@ -316,9 +315,9 @@ public final class SimpleCache implements Cache {
|
|||
}
|
||||
}
|
||||
}
|
||||
for (CacheSpan span : spansToBeRemoved) {
|
||||
for (int i = 0; i < spansToBeRemoved.size(); i++) {
|
||||
// Remove span but not CachedContent to prevent multiple index.store() calls.
|
||||
removeSpan(span, false);
|
||||
removeSpan(spansToBeRemoved.get(i), false);
|
||||
}
|
||||
index.removeEmpty();
|
||||
index.store();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util;
|
|||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -83,11 +84,27 @@ public final class CodecSpecificDataUtil {
|
|||
/**
|
||||
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param audioSpecificConfig The AudioSpecificConfig to parse.
|
||||
* @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse.
|
||||
* @return A pair consisting of the sample rate in Hz and the channel count.
|
||||
* @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
|
||||
*/
|
||||
public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig) {
|
||||
ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig);
|
||||
public static Pair<Integer, Integer> parseAacAudioSpecificConfig(byte[] audioSpecificConfig)
|
||||
throws ParserException {
|
||||
return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1
|
||||
*
|
||||
* @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The
|
||||
* position is advanced to the end of the AudioSpecificConfig.
|
||||
* @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for
|
||||
* knowing the length of the configuration payload.
|
||||
* @return A pair consisting of the sample rate in Hz and the channel count.
|
||||
* @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported.
|
||||
*/
|
||||
public static Pair<Integer, Integer> parseAacAudioSpecificConfig(ParsableBitArray bitArray,
|
||||
boolean forceReadToEnd) throws ParserException {
|
||||
int audioObjectType = getAacAudioObjectType(bitArray);
|
||||
int sampleRate = getAacSamplingFrequency(bitArray);
|
||||
int channelConfiguration = bitArray.readBits(4);
|
||||
|
|
@ -104,6 +121,41 @@ public final class CodecSpecificDataUtil {
|
|||
channelConfiguration = bitArray.readBits(4);
|
||||
}
|
||||
}
|
||||
|
||||
if (forceReadToEnd) {
|
||||
switch (audioObjectType) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 6:
|
||||
case 7:
|
||||
case 17:
|
||||
case 19:
|
||||
case 20:
|
||||
case 21:
|
||||
case 22:
|
||||
case 23:
|
||||
parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration);
|
||||
break;
|
||||
default:
|
||||
throw new ParserException("Unsupported audio object type: " + audioObjectType);
|
||||
}
|
||||
switch (audioObjectType) {
|
||||
case 17:
|
||||
case 19:
|
||||
case 20:
|
||||
case 21:
|
||||
case 22:
|
||||
case 23:
|
||||
int epConfig = bitArray.readBits(2);
|
||||
if (epConfig == 2 || epConfig == 3) {
|
||||
throw new ParserException("Unsupported epConfig: " + epConfig);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// For supported containers, bits_to_decode() is always 0.
|
||||
int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration];
|
||||
Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID);
|
||||
return Pair.create(sampleRate, channelCount);
|
||||
|
|
@ -269,4 +321,32 @@ public final class CodecSpecificDataUtil {
|
|||
return samplingFrequency;
|
||||
}
|
||||
|
||||
private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType,
|
||||
int channelConfiguration) {
|
||||
bitArray.skipBits(1); // frameLengthFlag.
|
||||
boolean dependsOnCoreDecoder = bitArray.readBit();
|
||||
if (dependsOnCoreDecoder) {
|
||||
bitArray.skipBits(14); // coreCoderDelay.
|
||||
}
|
||||
boolean extensionFlag = bitArray.readBit();
|
||||
if (channelConfiguration == 0) {
|
||||
throw new UnsupportedOperationException(); // TODO: Implement programConfigElement();
|
||||
}
|
||||
if (audioObjectType == 6 || audioObjectType == 20) {
|
||||
bitArray.skipBits(3); // layerNr.
|
||||
}
|
||||
if (extensionFlag) {
|
||||
if (audioObjectType == 22) {
|
||||
bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11).
|
||||
}
|
||||
if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20
|
||||
|| audioObjectType == 23) {
|
||||
// aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag,
|
||||
// aacSpectralDataResilienceFlag.
|
||||
bitArray.skipBits(3);
|
||||
}
|
||||
bitArray.skipBits(1); // extensionFlag3.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,43 @@ public final class ParsableBitArray {
|
|||
return returnValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads {@code numBits} bits into {@code buffer}.
|
||||
*
|
||||
* @param buffer The array into which the read data should be written. The trailing
|
||||
* {@code numBits % 8} bits are written into the most significant bits of the last modified
|
||||
* {@code buffer} byte. The remaining ones are unmodified.
|
||||
* @param offset The offset in {@code buffer} at which the read data should be written.
|
||||
* @param numBits The number of bits to read.
|
||||
*/
|
||||
public void readBits(byte[] buffer, int offset, int numBits) {
|
||||
// Whole bytes.
|
||||
int to = offset + (numBits >> 3) /* numBits / 8 */;
|
||||
for (int i = offset; i < to; i++) {
|
||||
buffer[i] = (byte) (data[byteOffset++] << bitOffset);
|
||||
buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset);
|
||||
}
|
||||
// Trailing bits.
|
||||
int bitsLeft = numBits & 7 /* numBits % 8 */;
|
||||
if (bitsLeft == 0) {
|
||||
return;
|
||||
}
|
||||
buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten.
|
||||
if (bitOffset + bitsLeft > 8) {
|
||||
// We read the rest of data[byteOffset] and increase byteOffset.
|
||||
buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset);
|
||||
bitOffset -= 8;
|
||||
}
|
||||
bitOffset += bitsLeft;
|
||||
int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset);
|
||||
buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft));
|
||||
if (bitOffset == 8) {
|
||||
bitOffset = 0;
|
||||
byteOffset++;
|
||||
}
|
||||
assertValidOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the position to the next byte boundary. Does nothing if the position is already aligned.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public final class DashUtilTest extends TestCase {
|
|||
|
||||
private static DrmInitData newDrmInitData() {
|
||||
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType",
|
||||
new byte[]{1, 4, 7, 0, 3, 6}));
|
||||
new byte[] {1, 4, 7, 0, 3, 6}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ android {
|
|||
dependencies {
|
||||
compile project(modulePrefix + 'library-core')
|
||||
compile 'com.android.support:support-annotations:' + supportLibraryVersion
|
||||
androidTestCompile project(modulePrefix + 'testutils')
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ import java.io.IOException;
|
|||
}
|
||||
|
||||
@Override
|
||||
public void skipData(long positionUs) {
|
||||
sampleStreamWrapper.skipData(group, positionUs);
|
||||
public int skipData(long positionUs) {
|
||||
return sampleStreamWrapper.skipData(group, positionUs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ import java.util.LinkedList;
|
|||
// sample queue, or if we haven't read anything from the queue since the previous seek
|
||||
// (this case is common for sparse tracks such as metadata tracks). In all other cases a
|
||||
// seek is required.
|
||||
seekRequired = !sampleQueue.advanceTo(positionUs, true, true)
|
||||
seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED
|
||||
&& sampleQueue.getReadIndex() != 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -320,6 +320,7 @@ import java.util.LinkedList;
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
if (loadingFinished) {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
|
|
@ -402,12 +403,13 @@ import java.util.LinkedList;
|
|||
lastSeekPositionUs);
|
||||
}
|
||||
|
||||
/* package */ void skipData(int trackGroupIndex, long positionUs) {
|
||||
/* package */ int skipData(int trackGroupIndex, long positionUs) {
|
||||
SampleQueue sampleQueue = sampleQueues[trackGroupIndex];
|
||||
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
|
||||
sampleQueue.advanceToEnd();
|
||||
return sampleQueue.advanceToEnd();
|
||||
} else {
|
||||
sampleQueue.advanceTo(positionUs, true, true);
|
||||
int skipCount = sampleQueue.advanceTo(positionUs, true, true);
|
||||
return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -760,7 +762,8 @@ import java.util.LinkedList;
|
|||
for (int i = 0; i < trackCount; i++) {
|
||||
SampleQueue sampleQueue = sampleQueues[i];
|
||||
sampleQueue.rewind();
|
||||
boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false);
|
||||
boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false)
|
||||
!= SampleQueue.ADVANCE_FAILED;
|
||||
// If we have AV tracks then an in-queue seek is successful if the seek into every AV queue
|
||||
// is successful. We ignore whether seeks within non-AV queues are successful in this case, as
|
||||
// they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
|||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -108,6 +109,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
|
|||
? Collections.unmodifiableList(muxedCaptionFormats) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this playlist which includes only the renditions identified by the given
|
||||
* urls.
|
||||
*
|
||||
* @param renditionUrls List of rendition urls.
|
||||
* @return A copy of this playlist which includes only the renditions identified by the given
|
||||
* urls.
|
||||
*/
|
||||
public HlsMasterPlaylist copy(List<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.
|
||||
*
|
||||
|
|
@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist {
|
|||
emptyList, null, null);
|
||||
}
|
||||
|
||||
private static List<HlsUrl> copyRenditionsList(List<HlsUrl> renditions, List<String> urls) {
|
||||
List<HlsUrl> copiedRenditions = new ArrayList<>(urls.size());
|
||||
for (int i = 0; i < renditions.size(); i++) {
|
||||
HlsUrl rendition = renditions.get(i);
|
||||
if (urls.contains(rendition.url)) {
|
||||
copiedRenditions.add(rendition);
|
||||
}
|
||||
}
|
||||
return copiedRenditions;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,8 +186,9 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener
|
|||
return "";
|
||||
}
|
||||
counters.ensureUpdated();
|
||||
return " rb:" + counters.renderedOutputBufferCount
|
||||
return " sib:" + counters.skippedInputBufferCount
|
||||
+ " sb:" + counters.skippedOutputBufferCount
|
||||
+ " rb:" + counters.renderedOutputBufferCount
|
||||
+ " db:" + counters.droppedOutputBufferCount
|
||||
+ " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<!-- 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>
|
||||
BIN
library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png
Normal file
|
After Width: | Height: | Size: 187 B |
BIN
library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png
Normal file
|
After Width: | Height: | Size: 230 B |
BIN
library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png
Normal file
|
After Width: | Height: | Size: 342 B |
BIN
library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png
Normal file
|
After Width: | Height: | Size: 436 B |
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Herhaal alles"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Herhaal niks"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Herhaal een"</string>
|
||||
<string name="exo_controls_shuffle_description">"Skommel"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"ሁሉንም ድገም"</string>
|
||||
<string name="exo_controls_repeat_off_description">"ምንም አትድገም"</string>
|
||||
<string name="exo_controls_repeat_one_description">"አንዱን ድገም"</string>
|
||||
<string name="exo_controls_shuffle_description">"በው"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"تكرار الكل"</string>
|
||||
<string name="exo_controls_repeat_off_description">"عدم التكرار"</string>
|
||||
<string name="exo_controls_repeat_one_description">"تكرار مقطع واحد"</string>
|
||||
<string name="exo_controls_shuffle_description">"ترتيب عشوائي"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Bütün təkrarlayın"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Təkrar bir"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Heç bir təkrar"</string>
|
||||
<string name="exo_controls_shuffle_description">"Qarışdır"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Ponovi sve"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Ne ponavljaj nijednu"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Ponovi jednu"</string>
|
||||
<string name="exo_controls_shuffle_description">"Pusti nasumično"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Паўтарыць усё"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Паўтараць ні"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Паўтарыць адзін"</string>
|
||||
<string name="exo_controls_shuffle_description">"Перамяшаць"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Повтаряне на всички"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Без повтаряне"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Повтаряне на един елемент"</string>
|
||||
<string name="exo_controls_shuffle_description">"Разбъркване"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"সবগুলির পুনরাবৃত্তি করুন"</string>
|
||||
<string name="exo_controls_repeat_off_description">"একটিরও পুনরাবৃত্তি করবেন না"</string>
|
||||
<string name="exo_controls_repeat_one_description">"একটির পুনরাবৃত্তি করুন"</string>
|
||||
<string name="exo_controls_shuffle_description">"অদলবদল"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Ponovite sve"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Ne ponavljaju"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Ponovite jedan"</string>
|
||||
<string name="exo_controls_shuffle_description">"Izmiješaj"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Repeteix-ho tot"</string>
|
||||
<string name="exo_controls_repeat_off_description">"No en repeteixis cap"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Repeteix-ne un"</string>
|
||||
<string name="exo_controls_shuffle_description">"Reprodueix aleatòriament"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Opakovat vše"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Neopakovat"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Opakovat jednu položku"</string>
|
||||
<string name="exo_controls_shuffle_description">"Náhodně"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Gentag alle"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Gentag ingen"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Gentag en"</string>
|
||||
<string name="exo_controls_shuffle_description">"Bland"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Alle wiederholen"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Keinen Titel wiederholen"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Einen Titel wiederholen"</string>
|
||||
<string name="exo_controls_shuffle_description">"Zufallsmix"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Επανάληψη όλων"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Καμία επανάληψη"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Επανάληψη ενός στοιχείου"</string>
|
||||
<string name="exo_controls_shuffle_description">"Τυχαία αναπαραγωγή"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
|
||||
<string name="exo_controls_shuffle_description">"Shuffle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
|
||||
<string name="exo_controls_shuffle_description">"Shuffle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Repeat all"</string>
|
||||
<string name="exo_controls_repeat_off_description">"Repeat none"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Repeat one"</string>
|
||||
<string name="exo_controls_shuffle_description">"Shuffle"</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@
|
|||
<string name="exo_controls_repeat_all_description">"Repetir todo"</string>
|
||||
<string name="exo_controls_repeat_off_description">"No repetir"</string>
|
||||
<string name="exo_controls_repeat_one_description">"Repetir uno"</string>
|
||||
<string name="exo_controls_shuffle_description">"Reproducir aleatoriamente"</string>
|
||||
</resources>
|
||||
|
|
|
|||