contentIdToDurationUsMap) {
+ int itemCount = items.size();
+ int index = 0;
+ idsToIndex = new SparseIntArray(itemCount);
+ ids = new int[itemCount];
+ durationsUs = new long[itemCount];
+ defaultPositionsUs = new long[itemCount];
+ for (MediaQueueItem item : items) {
+ int itemId = item.getItemId();
+ ids[index] = itemId;
+ idsToIndex.put(itemId, index);
+ MediaInfo mediaInfo = item.getMedia();
+ String contentId = mediaInfo.getContentId();
+ durationsUs[index] =
+ contentIdToDurationUsMap.containsKey(contentId)
+ ? contentIdToDurationUsMap.get(contentId)
+ : CastUtils.getStreamDurationUs(mediaInfo);
+ defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND);
+ index++;
+ }
+ }
+
+ // Timeline implementation.
+
+ @Override
+ public int getWindowCount() {
+ return ids.length;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ long durationUs = durationsUs[windowIndex];
+ boolean isDynamic = durationUs == C.TIME_UNSET;
+ return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic,
+ defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return ids.length;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ int id = ids[periodIndex];
+ return period.set(id, id, periodIndex, durationsUs[periodIndex], 0);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET;
+ }
+
+ // equals and hashCode implementations.
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof CastTimeline)) {
+ return false;
+ }
+ CastTimeline that = (CastTimeline) other;
+ return Arrays.equals(ids, that.ids)
+ && Arrays.equals(durationsUs, that.durationsUs)
+ && Arrays.equals(defaultPositionsUs, that.defaultPositionsUs);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Arrays.hashCode(ids);
+ result = 31 * result + Arrays.hashCode(durationsUs);
+ result = 31 * result + Arrays.hashCode(defaultPositionsUs);
+ return result;
+ }
+
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
new file mode 100644
index 0000000000..412bfb476d
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimelineTracker.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.ext.cast;
+
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.MediaStatus;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Creates {@link CastTimeline}s from cast receiver app media status.
+ *
+ * This class keeps track of the duration reported by the current item to fill any missing
+ * durations in the media queue items [See internal: b/65152553].
+ */
+/* package */ final class CastTimelineTracker {
+
+ private final HashMap contentIdToDurationUsMap;
+ private final HashSet scratchContentIdSet;
+
+ public CastTimelineTracker() {
+ contentIdToDurationUsMap = new HashMap<>();
+ scratchContentIdSet = new HashSet<>();
+ }
+
+ /**
+ * Returns a {@link CastTimeline} that represent the given {@code status}.
+ *
+ * @param status The Cast media status.
+ * @return A {@link CastTimeline} that represent the given {@code status}.
+ */
+ public CastTimeline getCastTimeline(MediaStatus status) {
+ MediaInfo mediaInfo = status.getMediaInfo();
+ List items = status.getQueueItems();
+ removeUnusedDurationEntries(items);
+
+ if (mediaInfo != null) {
+ String contentId = mediaInfo.getContentId();
+ long durationUs = CastUtils.getStreamDurationUs(mediaInfo);
+ contentIdToDurationUsMap.put(contentId, durationUs);
+ }
+ return new CastTimeline(items, contentIdToDurationUsMap);
+ }
+
+ private void removeUnusedDurationEntries(List items) {
+ scratchContentIdSet.clear();
+ for (MediaQueueItem item : items) {
+ scratchContentIdSet.add(item.getMedia().getContentId());
+ }
+ contentIdToDurationUsMap.keySet().retainAll(scratchContentIdSet);
+ }
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
new file mode 100644
index 0000000000..f17c39bdbf
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java
@@ -0,0 +1,110 @@
+/*
+ * 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.ext.cast;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.gms.cast.CastStatusCodes;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaTrack;
+
+/**
+ * Utility methods for ExoPlayer/Cast integration.
+ */
+/* package */ final class CastUtils {
+
+ /**
+ * Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
+ * unknown or not applicable.
+ *
+ * @param mediaInfo The media info to get the duration from.
+ * @return The duration in microseconds.
+ */
+ public static long getStreamDurationUs(MediaInfo mediaInfo) {
+ long durationMs =
+ mediaInfo != null ? mediaInfo.getStreamDuration() : MediaInfo.UNKNOWN_DURATION;
+ return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET;
+ }
+
+ /**
+ * Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
+ * {@link CastStatusCodes}.
+ *
+ * @param statusCode A Cast API status code.
+ * @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of
+ * {@link CastStatusCodes}.
+ */
+ public static String getLogString(int statusCode) {
+ switch (statusCode) {
+ case CastStatusCodes.APPLICATION_NOT_FOUND:
+ return "A requested application could not be found.";
+ case CastStatusCodes.APPLICATION_NOT_RUNNING:
+ return "A requested application is not currently running.";
+ case CastStatusCodes.AUTHENTICATION_FAILED:
+ return "Authentication failure.";
+ case CastStatusCodes.CANCELED:
+ return "An in-progress request has been canceled, most likely because another action has "
+ + "preempted it.";
+ case CastStatusCodes.ERROR_SERVICE_CREATION_FAILED:
+ return "The Cast Remote Display service could not be created.";
+ case CastStatusCodes.ERROR_SERVICE_DISCONNECTED:
+ return "The Cast Remote Display service was disconnected.";
+ case CastStatusCodes.FAILED:
+ return "The in-progress request failed.";
+ case CastStatusCodes.INTERNAL_ERROR:
+ return "An internal error has occurred.";
+ case CastStatusCodes.INTERRUPTED:
+ return "A blocking call was interrupted while waiting and did not run to completion.";
+ case CastStatusCodes.INVALID_REQUEST:
+ return "An invalid request was made.";
+ case CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL:
+ return "A message could not be sent because there is not enough room in the send buffer at "
+ + "this time.";
+ case CastStatusCodes.MESSAGE_TOO_LARGE:
+ return "A message could not be sent because it is too large.";
+ case CastStatusCodes.NETWORK_ERROR:
+ return "Network I/O error.";
+ case CastStatusCodes.NOT_ALLOWED:
+ return "The request was disallowed and could not be completed.";
+ case CastStatusCodes.REPLACED:
+ return "The request's progress is no longer being tracked because another request of the "
+ + "same type has been made before the first request completed.";
+ case CastStatusCodes.SUCCESS:
+ return "Success.";
+ case CastStatusCodes.TIMEOUT:
+ return "An operation has timed out.";
+ case CastStatusCodes.UNKNOWN_ERROR:
+ return "An unknown, unexpected error has occurred.";
+ default:
+ return "Unknown: " + statusCode;
+ }
+ }
+
+ /**
+ * Creates a {@link Format} instance containing all information contained in the given
+ * {@link MediaTrack} object.
+ *
+ * @param mediaTrack The {@link MediaTrack}.
+ * @return The equivalent {@link Format}.
+ */
+ public static Format mediaTrackToFormat(MediaTrack mediaTrack) {
+ return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(),
+ null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage());
+ }
+
+ private CastUtils() {}
+
+}
diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
new file mode 100644
index 0000000000..06f0bec971
--- /dev/null
+++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.ext.cast;
+
+import android.content.Context;
+import com.google.android.gms.cast.CastMediaControlIntent;
+import com.google.android.gms.cast.framework.CastOptions;
+import com.google.android.gms.cast.framework.OptionsProvider;
+import com.google.android.gms.cast.framework.SessionProvider;
+import java.util.List;
+
+/**
+ * A convenience {@link OptionsProvider} to target the default cast receiver app.
+ */
+public final class DefaultCastOptionsProvider implements OptionsProvider {
+
+ @Override
+ public CastOptions getCastOptions(Context context) {
+ return new CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
+ .setStopReceiverApplicationWhenEndingSession(true).build();
+ }
+
+ @Override
+ public List getAdditionalSessionProviders(Context context) {
+ return null;
+ }
+
+}
diff --git a/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
new file mode 100644
index 0000000000..bf4b20e156
--- /dev/null
+++ b/extensions/cast/src/test/java/com/google/android/exoplayer2/ext/cast/CastTimelineTrackerTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 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.ext.cast;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.testutil.TimelineAsserts;
+import com.google.android.gms.cast.MediaInfo;
+import com.google.android.gms.cast.MediaQueueItem;
+import com.google.android.gms.cast.MediaStatus;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+/** Tests for {@link CastTimelineTracker}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE)
+public class CastTimelineTrackerTest {
+
+ private static final long DURATION_1_MS = 1000;
+ private static final long DURATION_2_MS = 2000;
+ private static final long DURATION_3_MS = 3000;
+ private static final long DURATION_4_MS = 4000;
+ private static final long DURATION_5_MS = 5000;
+
+ /** Tests that duration of the current media info is correctly propagated to the timeline. */
+ @Test
+ public void testGetCastTimeline() {
+ MediaInfo mediaInfo;
+ MediaStatus status =
+ mockMediaStatus(
+ new int[] {1, 2, 3},
+ new String[] {"contentId1", "contentId2", "contentId3"},
+ new long[] {DURATION_1_MS, MediaInfo.UNKNOWN_DURATION, MediaInfo.UNKNOWN_DURATION});
+
+ CastTimelineTracker tracker = new CastTimelineTracker();
+ mediaInfo = mockMediaInfo("contentId1", DURATION_1_MS);
+ Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(status), C.msToUs(DURATION_1_MS), C.TIME_UNSET, C.TIME_UNSET);
+
+ mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
+ Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(status),
+ C.msToUs(DURATION_1_MS),
+ C.TIME_UNSET,
+ C.msToUs(DURATION_3_MS));
+
+ mediaInfo = mockMediaInfo("contentId2", DURATION_2_MS);
+ Mockito.when(status.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(status),
+ C.msToUs(DURATION_1_MS),
+ C.msToUs(DURATION_2_MS),
+ C.msToUs(DURATION_3_MS));
+
+ MediaStatus newStatus =
+ mockMediaStatus(
+ new int[] {4, 1, 5, 3},
+ new String[] {"contentId4", "contentId1", "contentId5", "contentId3"},
+ new long[] {
+ MediaInfo.UNKNOWN_DURATION,
+ MediaInfo.UNKNOWN_DURATION,
+ DURATION_5_MS,
+ MediaInfo.UNKNOWN_DURATION
+ });
+ mediaInfo = mockMediaInfo("contentId5", DURATION_5_MS);
+ Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(newStatus),
+ C.TIME_UNSET,
+ C.msToUs(DURATION_1_MS),
+ C.msToUs(DURATION_5_MS),
+ C.msToUs(DURATION_3_MS));
+
+ mediaInfo = mockMediaInfo("contentId3", DURATION_3_MS);
+ Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(newStatus),
+ C.TIME_UNSET,
+ C.msToUs(DURATION_1_MS),
+ C.msToUs(DURATION_5_MS),
+ C.msToUs(DURATION_3_MS));
+
+ mediaInfo = mockMediaInfo("contentId4", DURATION_4_MS);
+ Mockito.when(newStatus.getMediaInfo()).thenReturn(mediaInfo);
+ TimelineAsserts.assertPeriodDurations(
+ tracker.getCastTimeline(newStatus),
+ C.msToUs(DURATION_4_MS),
+ C.msToUs(DURATION_1_MS),
+ C.msToUs(DURATION_5_MS),
+ C.msToUs(DURATION_3_MS));
+ }
+
+ private static MediaStatus mockMediaStatus(
+ int[] itemIds, String[] contentIds, long[] durationsMs) {
+ ArrayList items = new ArrayList<>();
+ for (int i = 0; i < contentIds.length; i++) {
+ MediaInfo mediaInfo = mockMediaInfo(contentIds[i], durationsMs[i]);
+ MediaQueueItem item = Mockito.mock(MediaQueueItem.class);
+ Mockito.when(item.getMedia()).thenReturn(mediaInfo);
+ Mockito.when(item.getItemId()).thenReturn(itemIds[i]);
+ items.add(item);
+ }
+ MediaStatus status = Mockito.mock(MediaStatus.class);
+ Mockito.when(status.getQueueItems()).thenReturn(items);
+ return status;
+ }
+
+ private static MediaInfo mockMediaInfo(String contentId, long durationMs) {
+ MediaInfo mediaInfo = Mockito.mock(MediaInfo.class);
+ Mockito.when(mediaInfo.getContentId()).thenReturn(contentId);
+ Mockito.when(mediaInfo.getStreamDuration()).thenReturn(durationMs);
+ return mediaInfo;
+ }
+}
diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md
index 66da774978..ea84b602db 100644
--- a/extensions/cronet/README.md
+++ b/extensions/cronet/README.md
@@ -19,10 +19,20 @@ and enable the extension:
1. Copy the three jar files into the `libs` directory of this extension
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension
-
-* In your `settings.gradle` file, add
- `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
- applies `core_settings.gradle`.
+1. In your `settings.gradle` file, add
+ `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that
+ applies `core_settings.gradle`.
+1. In all `build.gradle` files where this extension is linked as a dependency,
+ add
+ ```
+ android {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ }
+ ```
+ to enable Java 8 features required by the Cronet library.
[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md
[here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android
diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml
index 6c4014873d..453cc68478 100644
--- a/extensions/cronet/src/androidTest/AndroidManifest.xml
+++ b/extensions/cronet/src/androidTest/AndroidManifest.xml
@@ -16,7 +16,7 @@
+ package="com.google.android.exoplayer2.ext.cronet">
@@ -28,6 +28,6 @@
+ android:targetPackage="com.google.android.exoplayer2.ext.cronet"/>
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
index bd81750fcb..28d22b91a5 100644
--- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
+++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java
@@ -15,8 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
+import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -53,13 +52,13 @@ public final class ByteArrayUploadDataProviderTest {
@Test
public void testGetLength() {
- assertEquals(TEST_DATA.length, byteArrayUploadDataProvider.getLength());
+ assertThat(byteArrayUploadDataProvider.getLength()).isEqualTo(TEST_DATA.length);
}
@Test
public void testReadFullBuffer() throws IOException {
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
- assertArrayEquals(TEST_DATA, byteBuffer.array());
+ assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
}
@Test
@@ -69,12 +68,12 @@ public final class ByteArrayUploadDataProviderTest {
byteBuffer = ByteBuffer.allocate(TEST_DATA.length / 2);
// Read half of the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
- assertArrayEquals(firstHalf, byteBuffer.array());
+ assertThat(byteBuffer.array()).isEqualTo(firstHalf);
// Read the second half of the data.
byteBuffer.rewind();
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
- assertArrayEquals(secondHalf, byteBuffer.array());
+ assertThat(byteBuffer.array()).isEqualTo(secondHalf);
verify(mockUploadDataSink, times(2)).onReadSucceeded(false);
}
@@ -82,13 +81,13 @@ public final class ByteArrayUploadDataProviderTest {
public void testRewind() throws IOException {
// Read all the data.
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
- assertArrayEquals(TEST_DATA, byteBuffer.array());
+ assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
// Rewind and make sure it can be read again.
byteBuffer.clear();
byteArrayUploadDataProvider.rewind(mockUploadDataSink);
byteArrayUploadDataProvider.read(mockUploadDataSink, byteBuffer);
- assertArrayEquals(TEST_DATA, byteBuffer.array());
+ assertThat(byteBuffer.array()).isEqualTo(TEST_DATA);
verify(mockUploadDataSink).onRewindSucceeded();
}
diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
index f92574b7ab..79be44398e 100644
--- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
+++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java
@@ -15,10 +15,7 @@
*/
package com.google.android.exoplayer2.ext.cronet;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
@@ -222,7 +219,7 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess();
- assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testDataSpec));
+ assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@@ -234,7 +231,7 @@ public final class CronetDataSourceTest {
testResponseHeader.put("Content-Length", Long.toString(50L));
mockResponseStartSuccess();
- assertEquals(5000 /* contentLength */, dataSourceUnderTest.open(testDataSpec));
+ assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@@ -247,7 +244,7 @@ public final class CronetDataSourceTest {
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
- assertFalse(e.getCause() instanceof UnknownHostException);
+ assertThat(e.getCause() instanceof UnknownHostException).isFalse();
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@@ -264,7 +261,7 @@ public final class CronetDataSourceTest {
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
// Check for connection not automatically closed.
- assertTrue(e.getCause() instanceof UnknownHostException);
+ assertThat(e.getCause() instanceof UnknownHostException).isTrue();
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
}
@@ -279,7 +276,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
- assertTrue(e instanceof HttpDataSource.InvalidResponseCodeException);
+ assertThat(e instanceof HttpDataSource.InvalidResponseCodeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
@@ -295,7 +292,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
fail("HttpDataSource.HttpDataSourceException expected");
} catch (HttpDataSourceException e) {
- assertTrue(e instanceof HttpDataSource.InvalidContentTypeException);
+ assertThat(e instanceof HttpDataSource.InvalidContentTypeException).isTrue();
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
verify(mockContentTypePredicate).evaluate(TEST_CONTENT_TYPE);
@@ -307,7 +304,7 @@ public final class CronetDataSourceTest {
mockResponseStartSuccess();
dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
- assertEquals(TEST_CONTENT_LENGTH, dataSourceUnderTest.open(testPostDataSpec));
+ assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
verify(mockTransferListener).onTransferStart(dataSourceUnderTest, testPostDataSpec);
}
@@ -346,13 +343,13 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
- assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
- assertEquals(8, bytesRead);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
returnedBuffer = new byte[8];
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
- assertArrayEquals(buildTestDataArray(8, 8), returnedBuffer);
- assertEquals(8, bytesRead);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));
+ assertThat(bytesRead).isEqualTo(8);
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
@@ -378,11 +375,11 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
- assertEquals(10, bytesRead);
+ assertThat(bytesRead).isEqualTo(10);
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
- assertEquals(6, bytesRead);
+ assertThat(bytesRead).isEqualTo(6);
bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
- assertEquals(C.RESULT_END_OF_INPUT, bytesRead);
+ assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
}
@Test
@@ -394,8 +391,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
- assertEquals(8, bytesRead);
- assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@@ -410,8 +407,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
- assertEquals(16, bytesRead);
- assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@@ -426,8 +423,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
- assertEquals(16, bytesRead);
- assertArrayEquals(buildTestDataArray(1000, 16), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@@ -441,8 +438,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[16];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
- assertArrayEquals(prefixZeros(buildTestDataArray(0, 8), 16), returnedBuffer);
- assertEquals(8, bytesRead);
+ assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
+ assertThat(bytesRead).isEqualTo(8);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 8);
}
@@ -455,8 +452,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[24];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
- assertArrayEquals(suffixZeros(buildTestDataArray(0, 16), 24), returnedBuffer);
- assertEquals(16, bytesRead);
+ assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
+ assertThat(bytesRead).isEqualTo(16);
verify(mockTransferListener).onBytesTransferred(dataSourceUnderTest, 16);
}
@@ -470,8 +467,8 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
- assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
- assertEquals(8, bytesRead);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
+ assertThat(bytesRead).isEqualTo(8);
dataSourceUnderTest.close();
verify(mockTransferListener).onTransferEnd(dataSourceUnderTest);
@@ -484,7 +481,7 @@ public final class CronetDataSourceTest {
}
// 16 bytes were attempted but only 8 should have been successfully read.
- assertEquals(8, bytesRead);
+ assertThat(bytesRead).isEqualTo(8);
}
@Test
@@ -498,20 +495,20 @@ public final class CronetDataSourceTest {
byte[] returnedBuffer = new byte[8];
int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
- assertEquals(8, bytesRead);
- assertArrayEquals(buildTestDataArray(0, 8), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(8);
+ assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
// The current buffer is kept if not completely consumed by DataSource reader.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
- assertEquals(14, bytesRead);
- assertArrayEquals(suffixZeros(buildTestDataArray(8, 6), 8), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(14);
+ assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));
// 2 bytes left at this point.
returnedBuffer = new byte[8];
bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
- assertEquals(16, bytesRead);
- assertArrayEquals(suffixZeros(buildTestDataArray(14, 2), 8), returnedBuffer);
+ assertThat(bytesRead).isEqualTo(16);
+ assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));
// Should have only called read on cronet once.
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
@@ -524,8 +521,8 @@ public final class CronetDataSourceTest {
// Return C.RESULT_END_OF_INPUT
returnedBuffer = new byte[16];
int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
- assertEquals(C.RESULT_END_OF_INPUT, bytesOverRead);
- assertArrayEquals(new byte[16], returnedBuffer);
+ assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
+ assertThat(returnedBuffer).isEqualTo(new byte[16]);
// C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
verify(mockTransferListener, never()).onBytesTransferred(dataSourceUnderTest,
C.RESULT_END_OF_INPUT);
@@ -533,7 +530,7 @@ public final class CronetDataSourceTest {
verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
// Check for connection not automatically closed.
verify(mockUrlRequest, never()).cancel();
- assertEquals(16, bytesRead);
+ assertThat(bytesRead).isEqualTo(16);
}
@Test
@@ -550,11 +547,10 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
- assertTrue(e instanceof CronetDataSource.OpenException);
- assertTrue(e.getCause() instanceof SocketTimeoutException);
- assertEquals(
- TEST_CONNECTION_STATUS,
- ((CronetDataSource.OpenException) e).cronetConnectionStatus);
+ assertThat(e instanceof CronetDataSource.OpenException).isTrue();
+ assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
+ assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
+ .isEqualTo(TEST_CONNECTION_STATUS);
timedOutCondition.open();
}
}
@@ -562,10 +558,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS);
timedOutCondition.block();
@@ -588,11 +584,10 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
- assertTrue(e instanceof CronetDataSource.OpenException);
- assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
- assertEquals(
- TEST_INVALID_CONNECTION_STATUS,
- ((CronetDataSource.OpenException) e).cronetConnectionStatus);
+ assertThat(e instanceof CronetDataSource.OpenException).isTrue();
+ assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
+ assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
+ .isEqualTo(TEST_INVALID_CONNECTION_STATUS);
timedOutCondition.open();
}
}
@@ -601,10 +596,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// Now we interrupt.
thread.interrupt();
timedOutCondition.block();
@@ -632,10 +627,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
- assertFalse(openCondition.block(50));
+ assertThat(openCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
- assertFalse(openCondition.block(50));
+ assertThat(openCondition.block(50)).isFalse();
// The response arrives just in time.
dataSourceUnderTest.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
openCondition.block();
@@ -656,8 +651,8 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
- assertTrue(e instanceof CronetDataSource.OpenException);
- assertTrue(e.getCause() instanceof SocketTimeoutException);
+ assertThat(e instanceof CronetDataSource.OpenException).isTrue();
+ assertThat(e.getCause() instanceof SocketTimeoutException).isTrue();
openExceptions.getAndIncrement();
timedOutCondition.open();
}
@@ -666,10 +661,10 @@ public final class CronetDataSourceTest {
startCondition.block();
// We should still be trying to open.
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// We should still be trying to open as we approach the timeout.
when(mockClock.elapsedRealtime()).thenReturn((long) TEST_CONNECT_TIMEOUT_MS - 1);
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl1");
@@ -677,9 +672,9 @@ public final class CronetDataSourceTest {
long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
- assertFalse(timedOutCondition.block(newTimeoutMs));
+ assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout.
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// A redirect arrives just in time.
dataSourceUnderTest.onRedirectReceived(mockUrlRequest, testUrlResponseInfo,
"RandomRedirectedUrl2");
@@ -687,15 +682,15 @@ public final class CronetDataSourceTest {
newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs - 1);
// Give the thread some time to run.
- assertFalse(timedOutCondition.block(newTimeoutMs));
+ assertThat(timedOutCondition.block(newTimeoutMs)).isFalse();
// We should still be trying to open as we approach the new timeout.
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// Now we timeout.
when(mockClock.elapsedRealtime()).thenReturn(newTimeoutMs);
timedOutCondition.block();
verify(mockTransferListener, never()).onTransferStart(dataSourceUnderTest, testDataSpec);
- assertEquals(1, openExceptions.get());
+ assertThat(openExceptions.get()).isEqualTo(1);
}
@Test
@@ -855,7 +850,7 @@ public final class CronetDataSourceTest {
fail();
} catch (HttpDataSourceException e) {
// Expected.
- assertTrue(e.getCause() instanceof CronetDataSource.InterruptedIOException);
+ assertThat(e.getCause() instanceof CronetDataSource.InterruptedIOException).isTrue();
timedOutCondition.open();
}
}
@@ -863,7 +858,7 @@ public final class CronetDataSourceTest {
thread.start();
startCondition.block();
- assertFalse(timedOutCondition.block(50));
+ assertThat(timedOutCondition.block(50)).isFalse();
// Now we interrupt.
thread.interrupt();
timedOutCondition.block();
diff --git a/extensions/cronet/src/main/AndroidManifest.xml b/extensions/cronet/src/main/AndroidManifest.xml
index c81d95f104..5ba54999f4 100644
--- a/extensions/cronet/src/main/AndroidManifest.xml
+++ b/extensions/cronet/src/main/AndroidManifest.xml
@@ -14,7 +14,7 @@
-->
+ package="com.google.android.exoplayer2.ext.cronet">
diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
index 536155a70f..29bc874cd8 100644
--- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
+++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java
@@ -369,6 +369,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
throw new HttpDataSourceException(exception, currentDataSpec,
HttpDataSourceException.TYPE_READ);
} else if (finished) {
+ bytesRemaining = 0;
return C.RESULT_END_OF_INPUT;
} else {
// The operation didn't time out, fail or finish, and therefore data must have been read.
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
index 8807738cfa..91bd82ab2a 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java
@@ -69,18 +69,23 @@ import java.util.List;
}
@Override
- public DecoderInputBuffer createInputBuffer() {
+ protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
@Override
- public SimpleOutputBuffer createOutputBuffer() {
+ protected SimpleOutputBuffer createOutputBuffer() {
return new SimpleOutputBuffer(this);
}
@Override
- public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer,
- SimpleOutputBuffer outputBuffer, boolean reset) {
+ protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) {
+ return new FfmpegDecoderException("Unexpected decode error", error);
+ }
+
+ @Override
+ protected FfmpegDecoderException decode(
+ DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
nativeContext = ffmpegReset(nativeContext, extraData);
if (nativeContext == 0) {
diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java
index b4cf327198..d6b5a62450 100644
--- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java
+++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java
@@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException {
super(message);
}
+ /* package */ FfmpegDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
index b03636f2bb..2a17cbdea6 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump
@@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
- getPosition(0) = 8880
+ getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
+ total output bytes = 526272
sample count = 33
sample 0:
time = 0
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
index 4e8388dba8..412e4a1b8f 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump
@@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
- getPosition(0) = 8880
+ getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
+ total output bytes = 362432
sample count = 23
sample 0:
time = 853333
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
index 0860c36cef..42ebb125d1 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump
@@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
- getPosition(0) = 8880
+ getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
+ total output bytes = 182208
sample count = 12
sample 0:
time = 1792000
diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
index 6f7f72b806..958cb0d418 100644
--- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump
+++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump
@@ -1,7 +1,7 @@
seekMap:
isSeekable = true
duration = 2741000
- getPosition(0) = 8880
+ getPosition(0) = [[timeUs=0, position=8880]]
numberOfTracks = 1
track 0:
format:
@@ -25,6 +25,7 @@ track 0:
language = null
drmInitData = -
initializationData:
+ total output bytes = 18368
sample count = 2
sample 0:
time = 2645333
diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
index 57ce487ac7..c5f1f5c146 100644
--- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
+++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java
@@ -34,11 +34,14 @@ public class FlacExtractorTest extends InstrumentationTestCase {
}
public void testSample() throws Exception {
- ExtractorAsserts.assertBehavior(new ExtractorFactory() {
- @Override
- public Extractor create() {
- return new FlacExtractor();
- }
- }, "bear.flac", getInstrumentation());
+ ExtractorAsserts.assertBehavior(
+ new ExtractorFactory() {
+ @Override
+ public Extractor create() {
+ return new FlacExtractor();
+ }
+ },
+ "bear.flac",
+ getInstrumentation().getContext());
}
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
index 3ecccd8246..15d294a35a 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java
@@ -70,18 +70,23 @@ import java.util.List;
}
@Override
- public DecoderInputBuffer createInputBuffer() {
+ protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
@Override
- public SimpleOutputBuffer createOutputBuffer() {
+ protected SimpleOutputBuffer createOutputBuffer() {
return new SimpleOutputBuffer(this);
}
@Override
- public FlacDecoderException decode(DecoderInputBuffer inputBuffer,
- SimpleOutputBuffer outputBuffer, boolean reset) {
+ protected FlacDecoderException createUnexpectedDecodeException(Throwable error) {
+ return new FlacDecoderException("Unexpected decode error", error);
+ }
+
+ @Override
+ protected FlacDecoderException decode(
+ DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
decoderJni.flush();
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java
index 2bdff62935..95d7f87c05 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java
@@ -26,4 +26,7 @@ public final class FlacDecoderException extends AudioDecoderException {
super(message);
}
+ /* package */ FlacDecoderException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
index a2f141a712..b630298c6e 100644
--- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
+++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java
@@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor {
}
metadataParsed = true;
- extractorOutput.seekMap(new SeekMap() {
- final boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
- final long durationUs = streamInfo.durationUs();
-
- @Override
- public boolean isSeekable() {
- return isSeekable;
- }
-
- @Override
- public long getPosition(long timeUs) {
- return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0;
- }
-
- @Override
- public long getDurationUs() {
- return durationUs;
- }
-
- });
+ boolean isSeekable = decoderJni.getSeekPosition(0) != -1;
+ extractorOutput.seekMap(
+ isSeekable
+ ? new FlacSeekMap(streamInfo.durationUs(), decoderJni)
+ : new SeekMap.Unseekable(streamInfo.durationUs(), 0));
Format mediaFormat =
Format.createAudioSampleFormat(
null,
@@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor {
}
}
+ private static final class FlacSeekMap implements SeekMap {
+
+ private final long durationUs;
+ private final FlacDecoderJni decoderJni;
+
+ public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
+ this.durationUs = durationUs;
+ this.decoderJni = decoderJni;
+ }
+
+ @Override
+ public boolean isSeekable() {
+ return true;
+ }
+
+ @Override
+ public SeekPoints getSeekPoints(long timeUs) {
+ // TODO: Access the seek table via JNI to return two seek points when appropriate.
+ return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs)));
+ }
+
+ @Override
+ public long getDurationUs() {
+ return durationUs;
+ }
+ }
}
diff --git a/extensions/flac/src/main/jni/flac_jni.cc b/extensions/flac/src/main/jni/flac_jni.cc
index c9e5d7ab36..59f37b0c2e 100644
--- a/extensions/flac/src/main/jni/flac_jni.cc
+++ b/extensions/flac/src/main/jni/flac_jni.cc
@@ -50,7 +50,8 @@ class JavaDataSource : public DataSource {
ssize_t readAt(off64_t offset, void *const data, size_t size) {
jobject byteBuffer = env->NewDirectByteBuffer(data, size);
int result = env->CallIntMethod(flacDecoderJni, mid, byteBuffer);
- if (env->ExceptionOccurred()) {
+ if (env->ExceptionCheck()) {
+ // Exception is thrown in Java when returning from the native call.
result = -1;
}
env->DeleteLocalRef(byteBuffer);
diff --git a/extensions/flac/src/main/jni/include/data_source.h b/extensions/flac/src/main/jni/include/data_source.h
index 175431dd7a..88af3e1277 100644
--- a/extensions/flac/src/main/jni/include/data_source.h
+++ b/extensions/flac/src/main/jni/include/data_source.h
@@ -22,6 +22,7 @@
class DataSource {
public:
+ virtual ~DataSource() {}
// Returns the number of bytes read, or -1 on failure. It's not an error if
// this returns zero; it just means the given offset is equal to, or
// beyond, the end of the source.
diff --git a/extensions/ima/proguard-rules.txt b/extensions/ima/proguard-rules.txt
new file mode 100644
index 0000000000..feef3daf7a
--- /dev/null
+++ b/extensions/ima/proguard-rules.txt
@@ -0,0 +1,6 @@
+# Proguard rules specific to the IMA extension.
+
+-keep class com.google.ads.interactivemedia.** { *; }
+-keep interface com.google.ads.interactivemedia.** { *; }
+-keep class com.google.obf.** { *; }
+-keep interface com.google.obf.** { *; }
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
index 70a8322bba..8ab05c574d 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java
@@ -25,6 +25,8 @@ import android.view.ViewGroup;
import android.webkit.WebView;
import com.google.ads.interactivemedia.v3.api.Ad;
import com.google.ads.interactivemedia.v3.api.AdDisplayContainer;
+import com.google.ads.interactivemedia.v3.api.AdError;
+import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener;
import com.google.ads.interactivemedia.v3.api.AdEvent;
@@ -48,6 +50,7 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ads.AdPlaybackState;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState.AdState;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
@@ -160,6 +163,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000;
+ /** The maximum duration before an ad break that IMA may start preloading the next ad. */
+ private static final long MAXIMUM_PRELOAD_DURATION_MS = 8000;
+
/**
* The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be
* clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in
@@ -211,6 +217,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
// Fields tracking IMA's state.
+ /** The expected ad group index that IMA should load next. */
+ private int expectedAdGroupIndex;
/**
* The index of the current ad group that IMA is loading.
*/
@@ -239,9 +247,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*/
private int playingAdIndexInAdGroup;
/**
- * If a content period has finished but IMA has not yet sent an ad event with
- * {@link AdEvent.AdEventType#CONTENT_PAUSE_REQUESTED}, stores the value of
- * {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
+ * Whether there's a pending ad preparation error which IMA needs to be notified of when it
+ * transitions from playing content to playing the ad.
+ */
+ private boolean shouldNotifyAdPrepareError;
+ /**
+ * If a content period has finished but IMA has not yet called {@link #playAd()}, stores the value
+ * of {@link SystemClock#elapsedRealtime()} when the content stopped playing. This can be used to
* determine a fake, increasing content position. {@link C#TIME_UNSET} otherwise.
*/
private long fakeContentProgressElapsedRealtimeMs;
@@ -332,7 +344,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
*
* Ads will be requested automatically when the player is prepared if this method has not been
* called, so it is only necessary to call this method if you want to request ads before preparing
- * the player
+ * the player.
*
* @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI.
*/
@@ -373,7 +385,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG,
MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG));
} else if (contentType == C.TYPE_SS) {
- // IMA does not support SmoothStreaming ad media.
+ // IMA does not support Smooth Streaming ad media.
}
}
this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes);
@@ -388,10 +400,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
lastContentProgress = null;
adDisplayContainer.setAdContainer(adUiViewGroup);
player.addListener(this);
- maybeNotifyAdError();
+ maybeNotifyPendingAdLoadError();
if (adPlaybackState != null) {
// Pass the ad playback state to the player, and resume ads if necessary.
- eventListener.onAdPlaybackState(adPlaybackState.copy());
+ eventListener.onAdPlaybackState(adPlaybackState);
if (imaPausedContent && player.getPlayWhenReady()) {
adsManager.resume();
}
@@ -407,7 +419,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void detachPlayer() {
if (adsManager != null && imaPausedContent) {
- adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
+ adPlaybackState =
+ adPlaybackState.withAdResumePositionUs(
+ playingAd ? C.msToUs(player.getCurrentPosition()) : 0);
adsManager.pause();
}
lastAdProgress = getAdProgress();
@@ -427,6 +441,18 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
+ @Override
+ public void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception) {
+ if (player == null) {
+ return;
+ }
+ try {
+ handleAdPrepareError(adGroupIndex, adIndexInAdGroup, exception);
+ } catch (Exception e) {
+ maybeNotifyInternalError("handlePrepareError", e);
+ }
+ }
+
// com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation.
@Override
@@ -442,7 +468,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsManager.addAdEventListener(this);
if (player != null) {
// If a player is attached already, start playback immediately.
- startAdPlayback();
+ try {
+ startAdPlayback();
+ } catch (Exception e) {
+ maybeNotifyInternalError("onAdsManagerLoaded", e);
+ }
}
}
@@ -451,65 +481,17 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onAdEvent(AdEvent adEvent) {
AdEventType adEventType = adEvent.getType();
- boolean isLogAdEvent = adEventType == AdEventType.LOG;
- if (DEBUG || isLogAdEvent) {
- Log.w(TAG, "onAdEvent: " + adEventType);
- if (isLogAdEvent) {
- for (Map.Entry entry : adEvent.getAdData().entrySet()) {
- Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue());
- }
- }
+ if (DEBUG) {
+ Log.d(TAG, "onAdEvent: " + adEventType);
}
if (adsManager == null) {
Log.w(TAG, "Dropping ad event after release: " + adEvent);
return;
}
- Ad ad = adEvent.getAd();
- switch (adEvent.getType()) {
- case LOADED:
- // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
- AdPodInfo adPodInfo = ad.getAdPodInfo();
- int podIndex = adPodInfo.getPodIndex();
- adGroupIndex =
- podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
- int adPosition = adPodInfo.getAdPosition();
- int adCount = adPodInfo.getTotalAds();
- adsManager.start();
- if (DEBUG) {
- Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
- }
- adPlaybackState.setAdCount(adGroupIndex, adCount);
- updateAdPlaybackState();
- break;
- case CONTENT_PAUSE_REQUESTED:
- // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
- // before sending CONTENT_RESUME_REQUESTED.
- imaPausedContent = true;
- pauseContentInternal();
- break;
- case STARTED:
- if (ad.isSkippable()) {
- focusSkipButton();
- }
- break;
- case TAPPED:
- if (eventListener != null) {
- eventListener.onAdTapped();
- }
- break;
- case CLICKED:
- if (eventListener != null) {
- eventListener.onAdClicked();
- }
- break;
- case CONTENT_RESUME_REQUESTED:
- imaPausedContent = false;
- resumeContentInternal();
- break;
- case ALL_ADS_COMPLETED:
- // Do nothing. The ads manager will be released when the source is released.
- default:
- break;
+ try {
+ handleAdEvent(adEvent);
+ } catch (Exception e) {
+ maybeNotifyInternalError("onAdEvent", e);
}
}
@@ -517,41 +499,68 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onAdError(AdErrorEvent adErrorEvent) {
+ AdError error = adErrorEvent.getError();
if (DEBUG) {
- Log.d(TAG, "onAdError " + adErrorEvent);
+ Log.d(TAG, "onAdError", error);
}
if (adsManager == null) {
// No ads were loaded, so allow playback to start without any ads.
pendingAdRequestContext = null;
- adPlaybackState = new AdPlaybackState(new long[0]);
+ adPlaybackState = new AdPlaybackState();
updateAdPlaybackState();
+ } else if (isAdGroupLoadError(error)) {
+ try {
+ handleAdGroupLoadError();
+ } catch (Exception e) {
+ maybeNotifyInternalError("onAdError", e);
+ }
}
if (pendingAdErrorEvent == null) {
pendingAdErrorEvent = adErrorEvent;
}
- maybeNotifyAdError();
+ maybeNotifyPendingAdLoadError();
}
// ContentProgressProvider implementation.
@Override
public VideoProgressUpdate getContentProgress() {
- boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
- long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
if (player == null) {
return lastContentProgress;
- } else if (pendingContentPositionMs != C.TIME_UNSET) {
+ }
+ boolean hasContentDuration = contentDurationMs != C.TIME_UNSET;
+ long contentPositionMs;
+ if (pendingContentPositionMs != C.TIME_UNSET) {
sentPendingContentPositionMs = true;
- return new VideoProgressUpdate(pendingContentPositionMs, contentDurationMs);
+ contentPositionMs = pendingContentPositionMs;
+ expectedAdGroupIndex =
+ adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
} else if (fakeContentProgressElapsedRealtimeMs != C.TIME_UNSET) {
long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs;
- long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
- return new VideoProgressUpdate(fakePositionMs, contentDurationMs);
- } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) {
- return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
+ contentPositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs;
+ expectedAdGroupIndex =
+ adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
+ } else if (imaAdState == IMA_AD_STATE_NONE && !playingAd && hasContentDuration) {
+ contentPositionMs = player.getCurrentPosition();
+ // Update the expected ad group index for the current content position. The update is delayed
+ // until MAXIMUM_PRELOAD_DURATION_MS before the ad so that an ad group load error delivered
+ // just after an ad group isn't incorrectly attributed to the next ad group.
+ int nextAdGroupIndex =
+ adPlaybackState.getAdGroupIndexAfterPositionUs(C.msToUs(contentPositionMs));
+ if (nextAdGroupIndex != expectedAdGroupIndex && nextAdGroupIndex != C.INDEX_UNSET) {
+ long nextAdGroupTimeMs = C.usToMs(adPlaybackState.adGroupTimesUs[nextAdGroupIndex]);
+ if (nextAdGroupTimeMs == C.TIME_END_OF_SOURCE) {
+ nextAdGroupTimeMs = contentDurationMs;
+ }
+ if (nextAdGroupTimeMs - contentPositionMs < MAXIMUM_PRELOAD_DURATION_MS) {
+ expectedAdGroupIndex = nextAdGroupIndex;
+ }
+ }
} else {
- return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs);
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
+ long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET;
+ return new VideoProgressUpdate(contentPositionMs, contentDurationMs);
}
// VideoAdPlayer implementation.
@@ -560,22 +569,40 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
public VideoProgressUpdate getAdProgress() {
if (player == null) {
return lastAdProgress;
- } else if (imaAdState == IMA_AD_STATE_NONE) {
- return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
- } else {
+ } else if (imaAdState != IMA_AD_STATE_NONE && playingAd) {
long adDuration = player.getDuration();
return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY
: new VideoProgressUpdate(player.getCurrentPosition(), adDuration);
+ } else {
+ return VideoProgressUpdate.VIDEO_TIME_NOT_READY;
}
}
@Override
public void loadAd(String adUriString) {
+ if (adGroupIndex == C.INDEX_UNSET) {
+ Log.w(
+ TAG,
+ "Unexpected loadAd without LOADED event; assuming ad group index is actually "
+ + expectedAdGroupIndex);
+ adGroupIndex = expectedAdGroupIndex;
+ adsManager.start();
+ }
if (DEBUG) {
Log.d(TAG, "loadAd in ad group " + adGroupIndex);
}
- adPlaybackState.addAdUri(adGroupIndex, Uri.parse(adUriString));
- updateAdPlaybackState();
+ try {
+ int adIndexInAdGroup = getAdIndexInAdGroupToLoad(adGroupIndex);
+ if (adIndexInAdGroup == C.INDEX_UNSET) {
+ Log.w(TAG, "Unexpected loadAd in an ad group with no remaining unavailable ads");
+ return;
+ }
+ adPlaybackState =
+ adPlaybackState.withAdUri(adGroupIndex, adIndexInAdGroup, Uri.parse(adUriString));
+ updateAdPlaybackState();
+ } catch (Exception e) {
+ maybeNotifyInternalError("loadAd", e);
+ }
}
@Override
@@ -600,10 +627,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.w(TAG, "Unexpected playAd without stopAd");
break;
case IMA_AD_STATE_NONE:
+ // IMA is requesting to play the ad, so stop faking the content position.
+ fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
+ fakeContentProgressOffsetMs = C.TIME_UNSET;
imaAdState = IMA_AD_STATE_PLAYING;
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onPlay();
}
+ if (shouldNotifyAdPrepareError) {
+ shouldNotifyAdPrepareError = false;
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onError();
+ }
+ }
break;
case IMA_AD_STATE_PAUSED:
imaAdState = IMA_AD_STATE_PLAYING;
@@ -635,7 +671,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.w(TAG, "Unexpected stopAd");
return;
}
- stopAdInternal();
+ try {
+ stopAdInternal();
+ } catch (Exception e) {
+ maybeNotifyInternalError("stopAd", e);
+ }
}
@Override
@@ -656,15 +696,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void resumeAd() {
// This method is never called. See [Internal: b/18931719].
- throw new IllegalStateException();
+ maybeNotifyInternalError("resumeAd", new IllegalStateException("Unexpected call to resumeAd"));
}
// Player.EventListener implementation.
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- if (timeline.isEmpty()) {
- // The player is being re-prepared and this source will be released.
+ public void onTimelineChanged(Timeline timeline, Object manifest,
+ @Player.TimelineChangeReason int reason) {
+ if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
+ // The player is being reset and this source will be released.
return;
}
Assertions.checkArgument(timeline.getPeriodCount() == 1);
@@ -672,7 +713,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
long contentDurationUs = timeline.getPeriod(0, period).durationUs;
contentDurationMs = C.usToMs(contentDurationUs);
if (contentDurationUs != C.TIME_UNSET) {
- adPlaybackState.contentDurationUs = contentDurationUs;
+ adPlaybackState = adPlaybackState.withContentDurationUs(contentDurationUs);
}
updateImaStateForPlayerState();
}
@@ -710,7 +751,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
@Override
public void onPlayerError(ExoPlaybackException error) {
- if (playingAd) {
+ if (imaAdState != IMA_AD_STATE_NONE) {
for (int i = 0; i < adCallbacks.size(); i++) {
adCallbacks.get(i).onError();
}
@@ -727,16 +768,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
if (sentContentComplete) {
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) {
- adPlaybackState.playedAdGroup(i);
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
}
updateAdPlaybackState();
} else {
long positionMs = player.getCurrentPosition();
timeline.getPeriod(0, period);
- if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) {
+ int newAdGroupIndex = period.getAdGroupIndexForPositionUs(C.msToUs(positionMs));
+ if (newAdGroupIndex != C.INDEX_UNSET) {
sentPendingContentPositionMs = false;
pendingContentPositionMs = positionMs;
+ if (newAdGroupIndex != adGroupIndex) {
+ shouldNotifyAdPrepareError = false;
+ }
}
}
} else {
@@ -753,21 +798,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
adsRenderingSettings.setMimeTypes(supportedMimeTypes);
// Set up the ad playback state, skipping ads based on the start position as required.
- pendingContentPositionMs = player.getCurrentPosition();
long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints());
adPlaybackState = new AdPlaybackState(adGroupTimesUs);
+ long contentPositionMs = player.getCurrentPosition();
int adGroupIndexForPosition =
- getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs));
+ adPlaybackState.getAdGroupIndexForPositionUs(C.msToUs(contentPositionMs));
if (adGroupIndexForPosition == 0) {
podIndexOffset = 0;
} else if (adGroupIndexForPosition == C.INDEX_UNSET) {
- pendingContentPositionMs = C.TIME_UNSET;
// There is no preroll and midroll pod indices start at 1.
podIndexOffset = -1;
} else /* adGroupIndexForPosition > 0 */ {
// Skip ad groups before the one at or immediately before the playback position.
for (int i = 0; i < adGroupIndexForPosition; i++) {
- adPlaybackState.playedAdGroup(i);
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
// Play ads after the midpoint between the ad to play and the one before it, to avoid issues
// with rounding one of the two ad times.
@@ -781,6 +825,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
podIndexOffset = adGroupIndexForPosition - 1;
}
+ if (adGroupIndexForPosition != C.INDEX_UNSET && hasMidrollAdGroups(adGroupTimesUs)) {
+ // Provide the player's initial position to trigger loading and playing the ad.
+ pendingContentPositionMs = contentPositionMs;
+ }
+
// Start ad playback.
adsManager.init(adsRenderingSettings);
updateAdPlaybackState();
@@ -789,12 +838,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
- private void maybeNotifyAdError() {
- if (eventListener != null && pendingAdErrorEvent != null) {
- IOException exception =
- new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError());
- eventListener.onLoadError(exception);
- pendingAdErrorEvent = null;
+ private void handleAdEvent(AdEvent adEvent) {
+ Ad ad = adEvent.getAd();
+ switch (adEvent.getType()) {
+ case LOADED:
+ // The ad position is not always accurate when using preloading. See [Internal: b/62613240].
+ AdPodInfo adPodInfo = ad.getAdPodInfo();
+ int podIndex = adPodInfo.getPodIndex();
+ adGroupIndex =
+ podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset);
+ int adPosition = adPodInfo.getAdPosition();
+ int adCount = adPodInfo.getTotalAds();
+ adsManager.start();
+ if (DEBUG) {
+ Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex);
+ }
+ int oldAdCount = adPlaybackState.adGroups[adGroupIndex].count;
+ if (adCount != oldAdCount) {
+ if (oldAdCount == C.LENGTH_UNSET) {
+ adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, adCount);
+ updateAdPlaybackState();
+ } else {
+ // IMA sometimes unexpectedly decreases the ad count in an ad group.
+ Log.w(TAG, "Unexpected ad count in LOADED, " + adCount + ", expected " + oldAdCount);
+ }
+ }
+ if (adGroupIndex != expectedAdGroupIndex) {
+ Log.w(
+ TAG,
+ "Expected ad group index "
+ + expectedAdGroupIndex
+ + ", actual ad group index "
+ + adGroupIndex);
+ expectedAdGroupIndex = adGroupIndex;
+ }
+ break;
+ case CONTENT_PAUSE_REQUESTED:
+ // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads
+ // before sending CONTENT_RESUME_REQUESTED.
+ imaPausedContent = true;
+ pauseContentInternal();
+ break;
+ case STARTED:
+ if (ad.isSkippable()) {
+ focusSkipButton();
+ }
+ break;
+ case TAPPED:
+ if (eventListener != null) {
+ eventListener.onAdTapped();
+ }
+ break;
+ case CLICKED:
+ if (eventListener != null) {
+ eventListener.onAdClicked();
+ }
+ break;
+ case CONTENT_RESUME_REQUESTED:
+ imaPausedContent = false;
+ resumeContentInternal();
+ break;
+ case LOG:
+ Map adData = adEvent.getAdData();
+ Log.i(TAG, "Log AdEvent: " + adData);
+ if ("adLoadError".equals(adData.get("type"))) {
+ handleAdGroupLoadError();
+ }
+ break;
+ case ALL_ADS_COMPLETED:
+ default:
+ break;
}
}
@@ -815,9 +928,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity");
}
}
- if (!wasPlayingAd && playingAd) {
+ if (!wasPlayingAd && playingAd && imaAdState == IMA_AD_STATE_NONE) {
int adGroupIndex = player.getCurrentAdGroupIndex();
- // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position.
+ // IMA hasn't called playAd yet, so fake the content position.
fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
@@ -834,8 +947,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd");
}
}
- if (playingAd && adGroupIndex != C.INDEX_UNSET) {
- adPlaybackState.playedAdGroup(adGroupIndex);
+ if (adGroupIndex != C.INDEX_UNSET) {
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(adGroupIndex);
adGroupIndex = C.INDEX_UNSET;
updateAdPlaybackState();
}
@@ -847,21 +960,76 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
pendingContentPositionMs = C.TIME_UNSET;
sentPendingContentPositionMs = false;
}
- // IMA is requesting to pause content, so stop faking the content position.
- fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET;
- fakeContentProgressOffsetMs = C.TIME_UNSET;
}
private void stopAdInternal() {
- Assertions.checkState(imaAdState != IMA_AD_STATE_NONE);
imaAdState = IMA_AD_STATE_NONE;
- adPlaybackState.playedAd(adGroupIndex);
+ int adIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
+ // TODO: Handle the skipped event so the ad can be marked as skipped rather than played.
+ adPlaybackState =
+ adPlaybackState.withPlayedAd(adGroupIndex, adIndexInAdGroup).withAdResumePositionUs(0);
updateAdPlaybackState();
if (!playingAd) {
adGroupIndex = C.INDEX_UNSET;
}
}
+ private void handleAdGroupLoadError() {
+ int adGroupIndex =
+ this.adGroupIndex == C.INDEX_UNSET ? expectedAdGroupIndex : this.adGroupIndex;
+ if (adGroupIndex == C.INDEX_UNSET) {
+ // Drop the error, as we don't know which ad group it relates to.
+ return;
+ }
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ if (adGroup.count == C.LENGTH_UNSET) {
+ adPlaybackState =
+ adPlaybackState.withAdCount(adGroupIndex, Math.max(1, adGroup.states.length));
+ adGroup = adPlaybackState.adGroups[adGroupIndex];
+ }
+ for (int i = 0; i < adGroup.count; i++) {
+ if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) {
+ if (DEBUG) {
+ Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex);
+ }
+ adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i);
+ }
+ }
+ updateAdPlaybackState();
+ }
+
+ private void handleAdPrepareError(int adGroupIndex, int adIndexInAdGroup, Exception exception) {
+ if (DEBUG) {
+ Log.d(
+ TAG, "Prepare error for ad " + adIndexInAdGroup + " in group " + adGroupIndex, exception);
+ }
+ if (imaAdState == IMA_AD_STATE_NONE) {
+ // Send IMA a content position at the ad group so that it will try to play it, at which point
+ // we can notify that it failed to load.
+ fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime();
+ fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]);
+ if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) {
+ fakeContentProgressOffsetMs = contentDurationMs;
+ }
+ shouldNotifyAdPrepareError = true;
+ } else {
+ // We're already playing an ad.
+ if (adIndexInAdGroup > playingAdIndexInAdGroup) {
+ // Mark the playing ad as ended so we can notify the error on the next ad and remove it,
+ // which means that the ad after will load (if any).
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onEnded();
+ }
+ }
+ playingAdIndexInAdGroup = adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
+ for (int i = 0; i < adCallbacks.size(); i++) {
+ adCallbacks.get(i).onError();
+ }
+ }
+ adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, adIndexInAdGroup);
+ updateAdPlaybackState();
+ }
+
private void checkForContentComplete() {
if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET
&& player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs
@@ -877,7 +1045,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
private void updateAdPlaybackState() {
// Ignore updates while detached. When a player is attached it will receive the latest state.
if (eventListener != null) {
- eventListener.onAdPlaybackState(adPlaybackState.copy());
+ eventListener.onAdPlaybackState(adPlaybackState);
}
}
@@ -890,6 +1058,48 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
}
}
+ /**
+ * Returns the next ad index in the specified ad group to load, or {@link C#INDEX_UNSET} if all
+ * ads in the ad group have loaded.
+ */
+ private int getAdIndexInAdGroupToLoad(int adGroupIndex) {
+ @AdState int[] states = adPlaybackState.adGroups[adGroupIndex].states;
+ int adIndexInAdGroup = 0;
+ // IMA loads ads in order.
+ while (adIndexInAdGroup < states.length
+ && states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE) {
+ adIndexInAdGroup++;
+ }
+ return adIndexInAdGroup == states.length ? C.INDEX_UNSET : adIndexInAdGroup;
+ }
+
+ private void maybeNotifyPendingAdLoadError() {
+ if (pendingAdErrorEvent != null) {
+ if (eventListener != null) {
+ eventListener.onAdLoadError(
+ new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()));
+ }
+ pendingAdErrorEvent = null;
+ }
+ }
+
+ private void maybeNotifyInternalError(String name, Exception cause) {
+ String message = "Internal error in " + name;
+ Log.e(TAG, message, cause);
+ if (eventListener != null) {
+ eventListener.onInternalAdLoadError(new RuntimeException(message, cause));
+ }
+ // We can't recover from an unexpected error in general, so skip all remaining ads.
+ if (adPlaybackState == null) {
+ adPlaybackState = new AdPlaybackState();
+ } else {
+ for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
+ adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
+ }
+ }
+ updateAdPlaybackState();
+ }
+
private static long[] getAdGroupTimesUs(List cuePoints) {
if (cuePoints.isEmpty()) {
// If no cue points are specified, there is a preroll ad.
@@ -898,28 +1108,35 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A
int count = cuePoints.size();
long[] adGroupTimesUs = new long[count];
+ int adGroupIndex = 0;
for (int i = 0; i < count; i++) {
double cuePoint = cuePoints.get(i);
- adGroupTimesUs[i] =
- cuePoint == -1.0 ? C.TIME_END_OF_SOURCE : (long) (C.MICROS_PER_SECOND * cuePoint);
+ if (cuePoint == -1.0) {
+ adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE;
+ } else {
+ adGroupTimesUs[adGroupIndex++] = (long) (C.MICROS_PER_SECOND * cuePoint);
+ }
}
+ // Cue points may be out of order, so sort them.
+ Arrays.sort(adGroupTimesUs, 0, adGroupIndex);
return adGroupTimesUs;
}
- /**
- * Returns the index of the ad group that should be played before playing the content at {@code
- * playbackPositionUs} when starting playback for the first time. This is the latest ad group at
- * or before the specified playback position. If the first ad is after the playback position,
- * returns {@link C#INDEX_UNSET}.
- */
- private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) {
- for (int i = 0; i < adGroupTimesUs.length; i++) {
- long adGroupTimeUs = adGroupTimesUs[i];
- // A postroll ad is after any position in the content.
- if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) {
- return i == 0 ? C.INDEX_UNSET : (i - 1);
- }
+ private static boolean isAdGroupLoadError(AdError adError) {
+ // TODO: Find out what other errors need to be handled (if any), and whether each one relates to
+ // a single ad, ad group or the whole timeline.
+ return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH;
+ }
+
+ private static boolean hasMidrollAdGroups(long[] adGroupTimesUs) {
+ int count = adGroupTimesUs.length;
+ if (count == 1) {
+ return adGroupTimesUs[0] != 0 && adGroupTimesUs[0] != C.TIME_END_OF_SOURCE;
+ } else if (count == 2) {
+ return adGroupTimesUs[0] != 0 || adGroupTimesUs[1] != C.TIME_END_OF_SOURCE;
+ } else {
+ // There's at least one midroll ad group, as adGroupTimesUs is never empty.
+ return true;
}
- return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1);
}
}
diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
index cd646daf42..981e8352e0 100644
--- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
+++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java
@@ -20,12 +20,12 @@ import android.support.annotation.Nullable;
import android.view.ViewGroup;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
-import java.io.IOException;
/**
* A {@link MediaSource} that inserts ads linearly with a provided content media source.
@@ -33,9 +33,10 @@ import java.io.IOException;
* @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader.
*/
@Deprecated
-public final class ImaAdsMediaSource implements MediaSource {
+public final class ImaAdsMediaSource extends CompositeMediaSource {
private final AdsMediaSource adsMediaSource;
+ private Listener listener;
/**
* Constructs a new source that inserts ads linearly with the content specified by
@@ -74,20 +75,10 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void prepareSource(final ExoPlayer player, boolean isTopLevelSource,
- final Listener listener) {
- adsMediaSource.prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
- @Nullable Object manifest) {
- listener.onSourceInfoRefreshed(ImaAdsMediaSource.this, timeline, manifest);
- }
- });
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- adsMediaSource.maybeThrowSourceInfoRefreshError();
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ super.prepareSource(player, isTopLevelSource, listener);
+ this.listener = listener;
+ prepareChildSource(/* id= */ null, adsMediaSource);
}
@Override
@@ -101,8 +92,8 @@ public final class ImaAdsMediaSource implements MediaSource {
}
@Override
- public void releaseSource() {
- adsMediaSource.releaseSource();
+ protected void onChildSourceInfoRefreshed(
+ Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
+ listener.onSourceInfoRefreshed(this, timeline, manifest);
}
-
}
diff --git a/extensions/ima/src/main/proguard-rules.txt b/extensions/ima/src/main/proguard-rules.txt
new file mode 100644
index 0000000000..feef3daf7a
--- /dev/null
+++ b/extensions/ima/src/main/proguard-rules.txt
@@ -0,0 +1,6 @@
+# Proguard rules specific to the IMA extension.
+
+-keep class com.google.ads.interactivemedia.** { *; }
+-keep interface com.google.ads.interactivemedia.** { *; }
+-keep class com.google.obf.** { *; }
+-keep interface com.google.obf.** { *; }
diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle
index 715e2e56d7..d8952ca2b8 100644
--- a/extensions/leanback/build.gradle
+++ b/extensions/leanback/build.gradle
@@ -30,7 +30,7 @@ dependencies {
}
ext {
- javadocTitle = 'Leanback extension for Exoplayer library'
+ javadocTitle = 'Leanback extension'
}
apply from: '../../javadoc_library.gradle'
diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
index 510ed9cf4f..e513084974 100644
--- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
+++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java
@@ -30,15 +30,15 @@ import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.DefaultControlDispatcher;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
+import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Player.DiscontinuityReason;
-import com.google.android.exoplayer2.SimpleExoPlayer;
+import com.google.android.exoplayer2.Player.TimelineChangeReason;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.util.ErrorMessageProvider;
+import com.google.android.exoplayer2.video.VideoListener;
-/**
- * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}.
- */
+/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */
public final class LeanbackPlayerAdapter extends PlayerAdapter {
static {
@@ -46,11 +46,12 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
private final Context context;
- private final SimpleExoPlayer player;
+ private final Player player;
private final Handler handler;
private final ComponentListener componentListener;
private final Runnable updateProgressRunnable;
+ private @Nullable PlaybackPreparer playbackPreparer;
private ControlDispatcher controlDispatcher;
private ErrorMessageProvider super ExoPlaybackException> errorMessageProvider;
private SurfaceHolderGlueHost surfaceHolderGlueHost;
@@ -59,14 +60,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
/**
* Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the
- * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when
- * it's no longer required.
+ * {@link Player} instance. The caller remains responsible for releasing the player when it's no
+ * longer required.
*
* @param context The current context (activity).
* @param player Instance of your exoplayer that needs to be configured.
* @param updatePeriodMs The delay between player control updates, in milliseconds.
*/
- public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) {
+ public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) {
this.context = context;
this.player = player;
handler = new Handler();
@@ -83,6 +84,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
};
}
+ /**
+ * Sets the {@link PlaybackPreparer}.
+ *
+ * @param playbackPreparer The {@link PlaybackPreparer}.
+ */
+ public void setPlaybackPreparer(@Nullable PlaybackPreparer playbackPreparer) {
+ this.playbackPreparer = playbackPreparer;
+ }
+
/**
* Sets the {@link ControlDispatcher}.
*
@@ -114,13 +124,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
notifyStateChanged();
player.addListener(componentListener);
- player.addVideoListener(componentListener);
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.addVideoListener(componentListener);
+ }
}
@Override
public void onDetachedFromHost() {
player.removeListener(componentListener);
- player.removeVideoListener(componentListener);
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.removeVideoListener(componentListener);
+ }
if (surfaceHolderGlueHost != null) {
surfaceHolderGlueHost.setSurfaceHolderCallback(null);
surfaceHolderGlueHost = null;
@@ -160,7 +176,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
@Override
public void play() {
- if (player.getPlaybackState() == Player.STATE_ENDED) {
+ if (player.getPlaybackState() == Player.STATE_IDLE) {
+ if (playbackPreparer != null) {
+ playbackPreparer.preparePlayback();
+ }
+ } else if (player.getPlaybackState() == Player.STATE_ENDED) {
controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET);
}
if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) {
@@ -195,7 +215,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
/* package */ void setVideoSurface(Surface surface) {
hasSurface = surface != null;
- player.setVideoSurface(surface);
+ Player.VideoComponent videoComponent = player.getVideoComponent();
+ if (videoComponent != null) {
+ videoComponent.setVideoSurface(surface);
+ }
maybeNotifyPreparedStateChanged(getCallback());
}
@@ -218,8 +241,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
}
- private final class ComponentListener extends Player.DefaultEventListener implements
- SimpleExoPlayer.VideoListener, SurfaceHolder.Callback {
+ private final class ComponentListener extends Player.DefaultEventListener
+ implements SurfaceHolder.Callback, VideoListener {
// SurfaceHolder.Callback implementation.
@@ -258,7 +281,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
}
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
+ public void onTimelineChanged(Timeline timeline, Object manifest,
+ @TimelineChangeReason int reason) {
Callback callback = getCallback();
callback.onDurationChanged(LeanbackPlayerAdapter.this);
callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this);
@@ -272,11 +296,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter {
callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this);
}
- // SimpleExoplayerView.Callback implementation.
+ // VideoListener implementation.
@Override
- public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
- float pixelWidthHeightRatio) {
+ public void onVideoSizeChanged(
+ int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height);
}
diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
index 1b1224273f..2b4409e0fb 100644
--- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
+++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java
@@ -655,7 +655,8 @@ public final class MediaSessionConnector {
private int currentWindowCount;
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
+ public void onTimelineChanged(Timeline timeline, Object manifest,
+ @Player.TimelineChangeReason int reason) {
int windowCount = player.getCurrentTimeline().getWindowCount();
int windowIndex = player.getCurrentWindowIndex();
if (queueNavigator != null) {
diff --git a/extensions/mediasession/src/main/res/values-af/strings.xml b/extensions/mediasession/src/main/res/values-af/strings.xml
index 4ef78cd84f..65bc1e89d8 100644
--- a/extensions/mediasession/src/main/res/values-af/strings.xml
+++ b/extensions/mediasession/src/main/res/values-af/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Herhaal alles"
- "Herhaal niks"
- "Herhaal een"
+ -->
+
+
+ "Herhaal niks"
+ "Herhaal een"
+ "Herhaal alles"
diff --git a/extensions/mediasession/src/main/res/values-am/strings.xml b/extensions/mediasession/src/main/res/values-am/strings.xml
index 531f605584..0dc20aaa04 100644
--- a/extensions/mediasession/src/main/res/values-am/strings.xml
+++ b/extensions/mediasession/src/main/res/values-am/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "ሁሉንም ድገም"
- "ምንም አትድገም"
- "አንዱን ድገም"
+ -->
+
+
+ "ምንም አትድገም"
+ "አንድ ድገም"
+ "ሁሉንም ድገም"
diff --git a/extensions/mediasession/src/main/res/values-ar/strings.xml b/extensions/mediasession/src/main/res/values-ar/strings.xml
index 0101a746e0..2776e28356 100644
--- a/extensions/mediasession/src/main/res/values-ar/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ar/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "تكرار الكل"
- "عدم التكرار"
- "تكرار مقطع واحد"
+ -->
+
+
+ "عدم التكرار"
+ "تكرار مقطع صوتي واحد"
+ "تكرار الكل"
diff --git a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
index 67a51cf85e..d20b16531a 100644
--- a/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
+++ b/extensions/mediasession/src/main/res/values-b+sr+Latn/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ponovi sve"
- "Ne ponavljaj nijednu"
- "Ponovi jednu"
+ -->
+
+
+ "Ne ponavljaj nijednu"
+ "Ponovi jednu"
+ "Ponovi sve"
diff --git a/extensions/mediasession/src/main/res/values-bg/strings.xml b/extensions/mediasession/src/main/res/values-bg/strings.xml
index 16910d640a..087eaee8c2 100644
--- a/extensions/mediasession/src/main/res/values-bg/strings.xml
+++ b/extensions/mediasession/src/main/res/values-bg/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Повтаряне на всички"
- "Без повтаряне"
- "Повтаряне на един елемент"
+ -->
+
+
+ "Без повтаряне"
+ "Повтаряне на един елемент"
+ "Повтаряне на всички"
diff --git a/extensions/mediasession/src/main/res/values-ca/strings.xml b/extensions/mediasession/src/main/res/values-ca/strings.xml
index 89414d736e..4a4d8646a2 100644
--- a/extensions/mediasession/src/main/res/values-ca/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ca/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repeteix-ho tot"
- "No en repeteixis cap"
- "Repeteix-ne un"
+ -->
+
+
+ "No en repeteixis cap"
+ "Repeteix una"
+ "Repeteix tot"
diff --git a/extensions/mediasession/src/main/res/values-cs/strings.xml b/extensions/mediasession/src/main/res/values-cs/strings.xml
index 784d872570..c59dcfc874 100644
--- a/extensions/mediasession/src/main/res/values-cs/strings.xml
+++ b/extensions/mediasession/src/main/res/values-cs/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Opakovat vše"
- "Neopakovat"
- "Opakovat jednu položku"
+ -->
+
+
+ "Neopakovat"
+ "Opakovat jednu"
+ "Opakovat vše"
diff --git a/extensions/mediasession/src/main/res/values-da/strings.xml b/extensions/mediasession/src/main/res/values-da/strings.xml
index 2c9784d122..0d31261f3d 100644
--- a/extensions/mediasession/src/main/res/values-da/strings.xml
+++ b/extensions/mediasession/src/main/res/values-da/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Gentag alle"
- "Gentag ingen"
- "Gentag en"
+ -->
+
+
+ "Gentag ingen"
+ "Gentag én"
+ "Gentag alle"
diff --git a/extensions/mediasession/src/main/res/values-de/strings.xml b/extensions/mediasession/src/main/res/values-de/strings.xml
index c11e449665..dfa86a54d4 100644
--- a/extensions/mediasession/src/main/res/values-de/strings.xml
+++ b/extensions/mediasession/src/main/res/values-de/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Alle wiederholen"
- "Keinen Titel wiederholen"
- "Einen Titel wiederholen"
+ -->
+
+
+ "Keinen wiederholen"
+ "Einen wiederholen"
+ "Alle wiederholen"
diff --git a/extensions/mediasession/src/main/res/values-el/strings.xml b/extensions/mediasession/src/main/res/values-el/strings.xml
index 6279af5d64..e73b24592e 100644
--- a/extensions/mediasession/src/main/res/values-el/strings.xml
+++ b/extensions/mediasession/src/main/res/values-el/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Επανάληψη όλων"
- "Καμία επανάληψη"
- "Επανάληψη ενός στοιχείου"
+ -->
+
+
+ "Καμία επανάληψη"
+ "Επανάληψη ενός κομματιού"
+ "Επανάληψη όλων"
diff --git a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
index a3fccf8b52..197222473d 100644
--- a/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rAU/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ -->
+
+
+ "Repeat none"
+ "Repeat one"
+ "Repeat all"
diff --git a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
index a3fccf8b52..197222473d 100644
--- a/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rGB/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ -->
+
+
+ "Repeat none"
+ "Repeat one"
+ "Repeat all"
diff --git a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
index a3fccf8b52..197222473d 100644
--- a/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
+++ b/extensions/mediasession/src/main/res/values-en-rIN/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repeat all"
- "Repeat none"
- "Repeat one"
+ -->
+
+
+ "Repeat none"
+ "Repeat one"
+ "Repeat all"
diff --git a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
index 0fe29d3d5a..192ad2f2ef 100644
--- a/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
+++ b/extensions/mediasession/src/main/res/values-es-rUS/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetir todo"
- "No repetir"
- "Repetir uno"
+ -->
+
+
+ "No repetir"
+ "Repetir uno"
+ "Repetir todo"
diff --git a/extensions/mediasession/src/main/res/values-es/strings.xml b/extensions/mediasession/src/main/res/values-es/strings.xml
index 0fe29d3d5a..192ad2f2ef 100644
--- a/extensions/mediasession/src/main/res/values-es/strings.xml
+++ b/extensions/mediasession/src/main/res/values-es/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetir todo"
- "No repetir"
- "Repetir uno"
+ -->
+
+
+ "No repetir"
+ "Repetir uno"
+ "Repetir todo"
diff --git a/extensions/mediasession/src/main/res/values-fa/strings.xml b/extensions/mediasession/src/main/res/values-fa/strings.xml
index e37a08de64..42b1b14c90 100644
--- a/extensions/mediasession/src/main/res/values-fa/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fa/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "تکرار همه"
- "تکرار هیچکدام"
- "یکبار تکرار"
+ -->
+
+
+ "تکرار هیچکدام"
+ "یکبار تکرار"
+ "تکرار همه"
diff --git a/extensions/mediasession/src/main/res/values-fi/strings.xml b/extensions/mediasession/src/main/res/values-fi/strings.xml
index c920827976..68f1b6c93b 100644
--- a/extensions/mediasession/src/main/res/values-fi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fi/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Toista kaikki"
- "Toista ei mitään"
- "Toista yksi"
+ -->
+
+
+ "Ei uudelleentoistoa"
+ "Toista yksi uudelleen"
+ "Toista kaikki uudelleen"
diff --git a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
index c5191e74a9..62edf759bb 100644
--- a/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fr-rCA/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Tout lire en boucle"
- "Aucune répétition"
- "Répéter un élément"
+ -->
+
+
+ "Ne rien lire en boucle"
+ "Lire une chanson en boucle"
+ "Tout lire en boucle"
diff --git a/extensions/mediasession/src/main/res/values-fr/strings.xml b/extensions/mediasession/src/main/res/values-fr/strings.xml
index 1d76358d1f..2ea8653e93 100644
--- a/extensions/mediasession/src/main/res/values-fr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-fr/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Tout lire en boucle"
- "Ne rien lire en boucle"
- "Lire en boucle un élément"
+ -->
+
+
+ "Ne rien lire en boucle"
+ "Lire un titre en boucle"
+ "Tout lire en boucle"
diff --git a/extensions/mediasession/src/main/res/values-hi/strings.xml b/extensions/mediasession/src/main/res/values-hi/strings.xml
index 8ce336d5e5..79261e4e59 100644
--- a/extensions/mediasession/src/main/res/values-hi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hi/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "सभी को दोहराएं"
- "कुछ भी न दोहराएं"
- "एक दोहराएं"
+ -->
+
+
+ "किसी को न दोहराएं"
+ "एक को दोहराएं"
+ "सभी को दोहराएं"
diff --git a/extensions/mediasession/src/main/res/values-hr/strings.xml b/extensions/mediasession/src/main/res/values-hr/strings.xml
index 9f995ec15b..81bb428528 100644
--- a/extensions/mediasession/src/main/res/values-hr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hr/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ponovi sve"
- "Bez ponavljanja"
- "Ponovi jedno"
+ -->
+
+
+ "Bez ponavljanja"
+ "Ponovi jedno"
+ "Ponovi sve"
diff --git a/extensions/mediasession/src/main/res/values-hu/strings.xml b/extensions/mediasession/src/main/res/values-hu/strings.xml
index 2335ade72e..8e8369a61f 100644
--- a/extensions/mediasession/src/main/res/values-hu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-hu/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Összes ismétlése"
- "Nincs ismétlés"
- "Egy ismétlése"
+ -->
+
+
+ "Nincs ismétlés"
+ "Egy szám ismétlése"
+ "Összes szám ismétlése"
diff --git a/extensions/mediasession/src/main/res/values-in/strings.xml b/extensions/mediasession/src/main/res/values-in/strings.xml
index 093a7f8576..a20a6362c8 100644
--- a/extensions/mediasession/src/main/res/values-in/strings.xml
+++ b/extensions/mediasession/src/main/res/values-in/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ulangi Semua"
- "Jangan Ulangi"
- "Ulangi Satu"
+ -->
+
+
+ "Jangan ulangi"
+ "Ulangi 1"
+ "Ulangi semua"
diff --git a/extensions/mediasession/src/main/res/values-it/strings.xml b/extensions/mediasession/src/main/res/values-it/strings.xml
index c0682519f9..3a59bb5804 100644
--- a/extensions/mediasession/src/main/res/values-it/strings.xml
+++ b/extensions/mediasession/src/main/res/values-it/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ripeti tutti"
- "Non ripetere nessuno"
- "Ripeti uno"
+ -->
+
+
+ "Non ripetere nulla"
+ "Ripeti uno"
+ "Ripeti tutto"
diff --git a/extensions/mediasession/src/main/res/values-iw/strings.xml b/extensions/mediasession/src/main/res/values-iw/strings.xml
index 5cf23d5a4c..f9eac73e59 100644
--- a/extensions/mediasession/src/main/res/values-iw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-iw/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "חזור על הכל"
- "אל תחזור על כלום"
- "חזור על פריט אחד"
+ -->
+
+
+ "אל תחזור על אף פריט"
+ "חזור על פריט אחד"
+ "חזור על הכול"
diff --git a/extensions/mediasession/src/main/res/values-ja/strings.xml b/extensions/mediasession/src/main/res/values-ja/strings.xml
index 6f543fbdee..bcfb6eb7c2 100644
--- a/extensions/mediasession/src/main/res/values-ja/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ja/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "全曲を繰り返し"
- "繰り返しなし"
- "1曲を繰り返し"
+ -->
+
+
+ "リピートなし"
+ "1 曲をリピート"
+ "全曲をリピート"
diff --git a/extensions/mediasession/src/main/res/values-ko/strings.xml b/extensions/mediasession/src/main/res/values-ko/strings.xml
index d269937771..7be13b133a 100644
--- a/extensions/mediasession/src/main/res/values-ko/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ko/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "전체 반복"
- "반복 안함"
- "한 항목 반복"
+ -->
+
+
+ "반복 안함"
+ "현재 미디어 반복"
+ "모두 반복"
diff --git a/extensions/mediasession/src/main/res/values-lt/strings.xml b/extensions/mediasession/src/main/res/values-lt/strings.xml
index ae8f1cf8c3..78d1753ed0 100644
--- a/extensions/mediasession/src/main/res/values-lt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lt/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Kartoti viską"
- "Nekartoti nieko"
- "Kartoti vieną"
+ -->
+
+
+ "Nekartoti nieko"
+ "Kartoti vieną"
+ "Kartoti viską"
diff --git a/extensions/mediasession/src/main/res/values-lv/strings.xml b/extensions/mediasession/src/main/res/values-lv/strings.xml
index a69f6a0ad5..085723a271 100644
--- a/extensions/mediasession/src/main/res/values-lv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-lv/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Atkārtot visu"
- "Neatkārtot nevienu"
- "Atkārtot vienu"
+ -->
+
+
+ "Neatkārtot nevienu"
+ "Atkārtot vienu"
+ "Atkārtot visu"
diff --git a/extensions/mediasession/src/main/res/values-nb/strings.xml b/extensions/mediasession/src/main/res/values-nb/strings.xml
index 10f334b226..2e986733fc 100644
--- a/extensions/mediasession/src/main/res/values-nb/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nb/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Gjenta alle"
- "Ikke gjenta noen"
- "Gjenta én"
+ -->
+
+
+ "Ikke gjenta noen"
+ "Gjenta én"
+ "Gjenta alle"
diff --git a/extensions/mediasession/src/main/res/values-nl/strings.xml b/extensions/mediasession/src/main/res/values-nl/strings.xml
index 55997be098..4dfc31bb98 100644
--- a/extensions/mediasession/src/main/res/values-nl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-nl/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Alles herhalen"
- "Niet herhalen"
- "Eén herhalen"
+ -->
+
+
+ "Niets herhalen"
+ "Eén herhalen"
+ "Alles herhalen"
diff --git a/extensions/mediasession/src/main/res/values-pl/strings.xml b/extensions/mediasession/src/main/res/values-pl/strings.xml
index 6a52d58b63..37af4c1616 100644
--- a/extensions/mediasession/src/main/res/values-pl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pl/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Powtórz wszystkie"
- "Nie powtarzaj"
- "Powtórz jeden"
+ -->
+
+
+ "Nie powtarzaj"
+ "Powtórz jeden"
+ "Powtórz wszystkie"
diff --git a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
index efb8fc433f..43a4cd9e6a 100644
--- a/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pt-rPT/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetir tudo"
- "Não repetir"
- "Repetir um"
+ -->
+
+
+ "Não repetir nenhum"
+ "Repetir um"
+ "Repetir tudo"
diff --git a/extensions/mediasession/src/main/res/values-pt/strings.xml b/extensions/mediasession/src/main/res/values-pt/strings.xml
index aadebbb3b0..4e7ce248cc 100644
--- a/extensions/mediasession/src/main/res/values-pt/strings.xml
+++ b/extensions/mediasession/src/main/res/values-pt/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetir tudo"
- "Não repetir"
- "Repetir uma"
+ -->
+
+
+ "Não repetir"
+ "Repetir uma"
+ "Repetir tudo"
diff --git a/extensions/mediasession/src/main/res/values-ro/strings.xml b/extensions/mediasession/src/main/res/values-ro/strings.xml
index f6aee447e5..9345a5df35 100644
--- a/extensions/mediasession/src/main/res/values-ro/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ro/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Repetați toate"
- "Repetați niciuna"
- "Repetați unul"
+ -->
+
+
+ "Nu repetați niciunul"
+ "Repetați unul"
+ "Repetați-le pe toate"
diff --git a/extensions/mediasession/src/main/res/values-ru/strings.xml b/extensions/mediasession/src/main/res/values-ru/strings.xml
index 575ad9f930..8c52ea8395 100644
--- a/extensions/mediasession/src/main/res/values-ru/strings.xml
+++ b/extensions/mediasession/src/main/res/values-ru/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Повторять все"
- "Не повторять"
- "Повторять один элемент"
+ -->
+
+
+ "Не повторять"
+ "Повторять трек"
+ "Повторять все"
diff --git a/extensions/mediasession/src/main/res/values-sk/strings.xml b/extensions/mediasession/src/main/res/values-sk/strings.xml
index 5d092003e5..9a7cccd096 100644
--- a/extensions/mediasession/src/main/res/values-sk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sk/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Opakovať všetko"
- "Neopakovať"
- "Opakovať jednu položku"
+ -->
+
+
+ "Neopakovať"
+ "Opakovať jednu"
+ "Opakovať všetko"
diff --git a/extensions/mediasession/src/main/res/values-sl/strings.xml b/extensions/mediasession/src/main/res/values-sl/strings.xml
index ecac3800c8..7bf20baa19 100644
--- a/extensions/mediasession/src/main/res/values-sl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sl/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ponovi vse"
- "Ne ponovi"
- "Ponovi eno"
+ -->
+
+
+ "Brez ponavljanja"
+ "Ponavljanje ene"
+ "Ponavljanje vseh"
diff --git a/extensions/mediasession/src/main/res/values-sr/strings.xml b/extensions/mediasession/src/main/res/values-sr/strings.xml
index 881cb2703b..b82940da2e 100644
--- a/extensions/mediasession/src/main/res/values-sr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sr/strings.xml
@@ -1,6 +1,5 @@
-
-
-
+ -->
+
+
+ "Не понављај ниједну"
+ "Понови једну"
+ "Понови све"
diff --git a/extensions/mediasession/src/main/res/values-sv/strings.xml b/extensions/mediasession/src/main/res/values-sv/strings.xml
index 3a7bb630aa..13edc46d1f 100644
--- a/extensions/mediasession/src/main/res/values-sv/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sv/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Upprepa alla"
- "Upprepa inga"
- "Upprepa en"
+ -->
+
+
+ "Upprepa inga"
+ "Upprepa en"
+ "Upprepa alla"
diff --git a/extensions/mediasession/src/main/res/values-sw/strings.xml b/extensions/mediasession/src/main/res/values-sw/strings.xml
index 726012ab88..b40ce1a727 100644
--- a/extensions/mediasession/src/main/res/values-sw/strings.xml
+++ b/extensions/mediasession/src/main/res/values-sw/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Rudia zote"
- "Usirudie Yoyote"
- "Rudia Moja"
+ -->
+
+
+ "Usirudie yoyote"
+ "Rudia moja"
+ "Rudia zote"
diff --git a/extensions/mediasession/src/main/res/values-th/strings.xml b/extensions/mediasession/src/main/res/values-th/strings.xml
index af502b3a4c..4e40f559d0 100644
--- a/extensions/mediasession/src/main/res/values-th/strings.xml
+++ b/extensions/mediasession/src/main/res/values-th/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "เล่นซ้ำทั้งหมด"
- "ไม่เล่นซ้ำ"
- "เล่นซ้ำรายการเดียว"
+ -->
+
+
+ "ไม่เล่นซ้ำ"
+ "เล่นซ้ำเพลงเดียว"
+ "เล่นซ้ำทั้งหมด"
diff --git a/extensions/mediasession/src/main/res/values-tl/strings.xml b/extensions/mediasession/src/main/res/values-tl/strings.xml
index 239972a4c7..4fff164f9f 100644
--- a/extensions/mediasession/src/main/res/values-tl/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tl/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Ulitin Lahat"
- "Walang Uulitin"
- "Ulitin ang Isa"
+ -->
+
+
+ "Walang uulitin"
+ "Mag-ulit ng isa"
+ "Ulitin lahat"
diff --git a/extensions/mediasession/src/main/res/values-tr/strings.xml b/extensions/mediasession/src/main/res/values-tr/strings.xml
index 89a98b1ed9..f93fd7fc80 100644
--- a/extensions/mediasession/src/main/res/values-tr/strings.xml
+++ b/extensions/mediasession/src/main/res/values-tr/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Tümünü Tekrarla"
- "Hiçbirini Tekrarlama"
- "Birini Tekrarla"
+ -->
+
+
+ "Hiçbirini tekrarlama"
+ "Bir şarkıyı tekrarla"
+ "Tümünü tekrarla"
diff --git a/extensions/mediasession/src/main/res/values-uk/strings.xml b/extensions/mediasession/src/main/res/values-uk/strings.xml
index 4e1d25eb8a..fb9d000474 100644
--- a/extensions/mediasession/src/main/res/values-uk/strings.xml
+++ b/extensions/mediasession/src/main/res/values-uk/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Повторити все"
- "Не повторювати"
- "Повторити один елемент"
+ -->
+
+
+ "Не повторювати"
+ "Повторити 1"
+ "Повторити всі"
diff --git a/extensions/mediasession/src/main/res/values-vi/strings.xml b/extensions/mediasession/src/main/res/values-vi/strings.xml
index dabc9e05d5..379dc36ee6 100644
--- a/extensions/mediasession/src/main/res/values-vi/strings.xml
+++ b/extensions/mediasession/src/main/res/values-vi/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Lặp lại tất cả"
- "Không lặp lại"
- "Lặp lại một mục"
+ -->
+
+
+ "Không lặp lại"
+ "Lặp lại một"
+ "Lặp lại tất cả"
diff --git a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
index beb3403cb9..6917f75bf9 100644
--- a/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rCN/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "重复播放全部"
- "不重复播放"
- "重复播放单个视频"
+ -->
+
+
+ "不重复播放"
+ "重复播放一项"
+ "全部重复播放"
diff --git a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
index 775cd6441c..b63f103e2a 100644
--- a/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rHK/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "重複播放所有媒體項目"
- "不重複播放任何媒體項目"
- "重複播放一個媒體項目"
+ -->
+
+
+ "不重複播放"
+ "重複播放一個"
+ "全部重複播放"
diff --git a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
index d3789f4145..0a460b9e08 100644
--- a/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zh-rTW/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "重複播放所有媒體項目"
- "不重複播放"
- "重複播放單一媒體項目"
+ -->
+
+
+ "不重複播放"
+ "重複播放單一項目"
+ "重複播放所有項目"
diff --git a/extensions/mediasession/src/main/res/values-zu/strings.xml b/extensions/mediasession/src/main/res/values-zu/strings.xml
index 789b6fecb4..ccf8452d69 100644
--- a/extensions/mediasession/src/main/res/values-zu/strings.xml
+++ b/extensions/mediasession/src/main/res/values-zu/strings.xml
@@ -1,6 +1,5 @@
-
-
-
- "Phinda konke"
- "Ungaphindi lutho"
- "Phida okukodwa"
+ -->
+
+
+ "Phinda okungekho"
+ "Phinda okukodwa"
+ "Phinda konke"
diff --git a/extensions/mediasession/src/main/res/values/strings.xml b/extensions/mediasession/src/main/res/values/strings.xml
index 72a67ff01c..015fd04cea 100644
--- a/extensions/mediasession/src/main/res/values/strings.xml
+++ b/extensions/mediasession/src/main/res/values/strings.xml
@@ -14,7 +14,10 @@
limitations under the License.
-->
+
Repeat none
+
Repeat one
+
Repeat all
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
index b4a4622346..f8ec477b88 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java
@@ -135,18 +135,23 @@ import java.util.List;
}
@Override
- public DecoderInputBuffer createInputBuffer() {
+ protected DecoderInputBuffer createInputBuffer() {
return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT);
}
@Override
- public SimpleOutputBuffer createOutputBuffer() {
+ protected SimpleOutputBuffer createOutputBuffer() {
return new SimpleOutputBuffer(this);
}
@Override
- public OpusDecoderException decode(DecoderInputBuffer inputBuffer,
- SimpleOutputBuffer outputBuffer, boolean reset) {
+ protected OpusDecoderException createUnexpectedDecodeException(Throwable error) {
+ return new OpusDecoderException("Unexpected decode error", error);
+ }
+
+ @Override
+ protected OpusDecoderException decode(
+ DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) {
if (reset) {
opusReset(nativeDecoderContext);
// When seeking to 0, skip number of samples as specified in opus header. When seeking to
diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
index 22985ea497..4cb3ce3190 100644
--- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
+++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java
@@ -27,7 +27,7 @@ public final class OpusLibrary {
ExoPlayerLibraryInfo.registerModule("goog.exo.opus");
}
- private static final LibraryLoader LOADER = new LibraryLoader("opus", "opusJNI");
+ private static final LibraryLoader LOADER = new LibraryLoader("opusJNI");
private OpusLibrary() {}
diff --git a/extensions/opus/src/main/jni/Android.mk b/extensions/opus/src/main/jni/Android.mk
index 2ceb8fc4f7..9d1e4fe726 100644
--- a/extensions/opus/src/main/jni/Android.mk
+++ b/extensions/opus/src/main/jni/Android.mk
@@ -17,7 +17,7 @@
WORKING_DIR := $(call my-dir)
include $(CLEAR_VARS)
-# build libopus.so
+# build libopus.a
LOCAL_PATH := $(WORKING_DIR)
include libopus.mk
@@ -29,5 +29,5 @@ LOCAL_ARM_MODE := arm
LOCAL_CPP_EXTENSION := .cc
LOCAL_SRC_FILES := opus_jni.cc
LOCAL_LDLIBS := -llog -lz -lm
-LOCAL_SHARED_LIBRARIES := libopus
+LOCAL_STATIC_LIBRARIES := libopus
include $(BUILD_SHARED_LIBRARY)
diff --git a/extensions/opus/src/main/jni/libopus.mk b/extensions/opus/src/main/jni/libopus.mk
index 0a5dd15b5a..672df600c0 100644
--- a/extensions/opus/src/main/jni/libopus.mk
+++ b/extensions/opus/src/main/jni/libopus.mk
@@ -47,4 +47,4 @@ endif
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
-include $(BUILD_SHARED_LIBRARY)
+include $(BUILD_STATIC_LIBRARY)
diff --git a/extensions/opus/src/main/jni/opus_jni.cc b/extensions/opus/src/main/jni/opus_jni.cc
index 8d9c1a4152..9042e4cb89 100644
--- a/extensions/opus/src/main/jni/opus_jni.cc
+++ b/extensions/opus/src/main/jni/opus_jni.cc
@@ -103,8 +103,16 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs,
kMaxOpusOutputPacketSizeSamples * kBytesPerSample * channelCount;
env->CallObjectMethod(jOutputBuffer, outputBufferInit, jTimeUs, outputSize);
+ if (env->ExceptionCheck()) {
+ // Exception is thrown in Java when returning from the native call.
+ return -1;
+ }
const jobject jOutputBufferData = env->CallObjectMethod(jOutputBuffer,
outputBufferInit, jTimeUs, outputSize);
+ if (env->ExceptionCheck()) {
+ // Exception is thrown in Java when returning from the native call.
+ return -1;
+ }
int16_t* outputBufferData = reinterpret_cast(
env->GetDirectBufferAddress(jOutputBufferData));
diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle
index 7687f03e32..2afa4a4ea7 100644
--- a/extensions/rtmp/build.gradle
+++ b/extensions/rtmp/build.gradle
@@ -26,7 +26,7 @@ android {
dependencies {
compile project(modulePrefix + 'library-core')
- compile 'net.butterflytv.utils:rtmp-client:3.0.0'
+ compile 'net.butterflytv.utils:rtmp-client:3.0.1'
}
ext {
diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md
index 649e4a6ee2..9601829c91 100644
--- a/extensions/vp9/README.md
+++ b/extensions/vp9/README.md
@@ -29,7 +29,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main"
```
* Download the [Android NDK][] and set its location in an environment variable.
-Only versions up to NDK 15c are supported currently (see [#3520][]).
```
NDK_PATH=""
diff --git a/extensions/vp9/build.gradle b/extensions/vp9/build.gradle
index de6dc65f74..3d68e1428f 100644
--- a/extensions/vp9/build.gradle
+++ b/extensions/vp9/build.gradle
@@ -32,6 +32,7 @@ android {
dependencies {
compile project(modulePrefix + 'library-core')
+ androidTestCompile 'com.google.truth:truth:' + truthVersion
}
ext {
diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
index 0a902e2efe..09701f9542 100644
--- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
+++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.ext.vp9;
+import static com.google.common.truth.Truth.assertThat;
+
import android.content.Context;
import android.net.Uri;
import android.os.Looper;
@@ -73,8 +75,8 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
playUri(INVALID_BITSTREAM_URI);
fail();
} catch (Exception e) {
- assertNotNull(e.getCause());
- assertTrue(e.getCause() instanceof VpxDecoderException);
+ assertThat(e.getCause()).isNotNull();
+ assertThat(e.getCause()).isInstanceOf(VpxDecoderException.class);
}
}
@@ -119,9 +121,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"))
.setExtractorsFactory(MatroskaExtractor.FACTORY)
.createMediaSource(uri);
- player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer,
- LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER,
- new VpxVideoSurfaceView(context)));
+ player
+ .createMessage(videoRenderer)
+ .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER)
+ .setPayload(new VpxVideoSurfaceView(context))
+ .send();
player.prepare(mediaSource);
player.setPlayWhenReady(true);
Looper.loop();
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
index dd303af0d8..d93aa6d39e 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java
@@ -20,7 +20,9 @@ import android.graphics.Canvas;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
+import android.support.annotation.CallSuper;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
@@ -28,6 +30,7 @@ import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
@@ -45,8 +48,19 @@ import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders video using the native VP9 decoder.
+ *
+ * This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ *
+ * - Message with type {@link C#MSG_SET_SURFACE} to set the output surface. The message payload
+ * should be the target {@link Surface}, or null.
+ *
- Message with type {@link #MSG_SET_OUTPUT_BUFFER_RENDERER} to set the output buffer
+ * renderer. The message payload should be the target {@link VpxOutputBufferRenderer}, or
+ * null.
+ *
*/
-public final class LibvpxVideoRenderer extends BaseRenderer {
+public class LibvpxVideoRenderer extends BaseRenderer {
@Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
@@ -70,9 +84,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
/**
- * The type of a message that can be passed to an instance of this class via
- * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be the target {@link VpxOutputBufferRenderer}, or null.
+ * The type of a message that can be passed to an instance of this class via {@link
+ * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link
+ * VpxOutputBufferRenderer}, or null.
*/
public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE;
@@ -84,7 +98,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
* The number of output buffers. The renderer may limit the minimum possible value due to
* requiring multiple output buffers to be dequeued at a time for it to make progress.
*/
- private static final int NUM_OUTPUT_BUFFERS = 16;
+ private static final int NUM_OUTPUT_BUFFERS = 8;
/**
* The initial input buffer size. Input buffers are reallocated dynamically if this value is
* insufficient.
@@ -92,6 +106,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp.
private final boolean scaleToFit;
+ private final boolean disableLoopFilter;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
private final boolean playClearSamplesWithoutKeys;
@@ -100,7 +115,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager drmSessionManager;
- private DecoderCounters decoderCounters;
private Format format;
private VpxDecoder decoder;
private VpxInputBuffer inputBuffer;
@@ -131,6 +145,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private int consecutiveDroppedFrameCount;
private int buffersInCodecCount;
+ protected DecoderCounters decoderCounters;
+
/**
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
@@ -154,7 +170,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
Handler eventHandler, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify,
- null, false);
+ null, false, false);
}
/**
@@ -173,13 +189,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
+ * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
*/
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
Handler eventHandler, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager,
- boolean playClearSamplesWithoutKeys) {
+ boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) {
super(C.TRACK_TYPE_VIDEO);
this.scaleToFit = scaleToFit;
+ this.disableLoopFilter = disableLoopFilter;
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager;
@@ -193,6 +211,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
}
+ // BaseRenderer implementation.
+
@Override
public int supportsFormat(Format format) {
if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) {
@@ -244,273 +264,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException,
- VpxDecoderException {
- // Acquire outputBuffer either from nextOutputBuffer or from the decoder.
- if (outputBuffer == null) {
- if (nextOutputBuffer != null) {
- outputBuffer = nextOutputBuffer;
- nextOutputBuffer = null;
- } else {
- outputBuffer = decoder.dequeueOutputBuffer();
- }
- if (outputBuffer == null) {
- return false;
- }
- decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
- buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
- }
-
- if (nextOutputBuffer == null) {
- nextOutputBuffer = decoder.dequeueOutputBuffer();
- }
-
- if (outputBuffer.isEndOfStream()) {
- if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
- // We're waiting to re-initialize the decoder, and have now processed all final buffers.
- releaseDecoder();
- maybeInitDecoder();
- } else {
- outputBuffer.release();
- outputBuffer = null;
- outputStreamEnded = true;
- }
- return false;
- }
-
- if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
- // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
- if (isBufferLate(outputBuffer.timeUs - positionUs)) {
- forceRenderFrame = false;
- skipBuffer();
- buffersInCodecCount--;
- return true;
- }
- return false;
- }
-
- if (forceRenderFrame) {
- forceRenderFrame = false;
- renderBuffer();
- buffersInCodecCount--;
- return true;
- }
-
- final long nextOutputBufferTimeUs =
- nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream()
- ? nextOutputBuffer.timeUs : C.TIME_UNSET;
-
- long earlyUs = outputBuffer.timeUs - positionUs;
- if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
- forceRenderFrame = true;
- return false;
- } else if (shouldDropOutputBuffer(
- outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
- dropBuffer();
- buffersInCodecCount--;
- return true;
- }
-
- // If we have yet to render a frame to the current output (either initially or immediately
- // following a seek), render one irrespective of the state or current position.
- if (!renderedFirstFrame
- || (getState() == STATE_STARTED && earlyUs <= 30000)) {
- renderBuffer();
- buffersInCodecCount--;
- }
- return false;
- }
-
- /**
- * Returns whether the current frame should be dropped.
- *
- * @param outputBufferTimeUs The timestamp of the current output buffer.
- * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET}
- * if the next output buffer is unavailable.
- * @param positionUs The current playback position.
- * @param joiningDeadlineMs The joining deadline.
- * @return Returns whether to drop the current output buffer.
- */
- private boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs,
- long positionUs, long joiningDeadlineMs) {
- return isBufferLate(outputBufferTimeUs - positionUs)
- && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
- }
-
- /**
- * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after
- * the current playback position, if possible.
- *
- * @param earlyUs The time until the current buffer should be presented in microseconds. A
- * negative value indicates that the buffer is late.
- */
- private boolean shouldDropBuffersToKeyframe(long earlyUs) {
- return isBufferVeryLate(earlyUs);
- }
-
- private void renderBuffer() {
- int bufferMode = outputBuffer.mode;
- boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
- boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
- if (!renderRgb && !renderYuv) {
- dropBuffer();
- } else {
- maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
- if (renderRgb) {
- renderRgbFrame(outputBuffer, scaleToFit);
- outputBuffer.release();
- } else /* renderYuv */ {
- outputBufferRenderer.setOutputBuffer(outputBuffer);
- // The renderer will release the buffer.
- }
- outputBuffer = null;
- consecutiveDroppedFrameCount = 0;
- decoderCounters.renderedOutputBufferCount++;
- maybeNotifyRenderedFirstFrame();
- }
- }
-
- private void dropBuffer() {
- updateDroppedBufferCounters(1);
- outputBuffer.release();
- outputBuffer = null;
- }
-
- private boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException {
- int droppedSourceBufferCount = skipSource(positionUs);
- if (droppedSourceBufferCount == 0) {
- return false;
- }
- decoderCounters.droppedToKeyframeCount++;
- // We dropped some buffers to catch up, so update the decoder counters and flush the codec,
- // which releases all pending buffers buffers including the current output buffer.
- updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);
- flushDecoder();
- return true;
- }
-
- private void updateDroppedBufferCounters(int droppedBufferCount) {
- decoderCounters.droppedBufferCount += droppedBufferCount;
- droppedFrames += droppedBufferCount;
- consecutiveDroppedFrameCount += droppedBufferCount;
- decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount,
- decoderCounters.maxConsecutiveDroppedBufferCount);
- if (droppedFrames >= maxDroppedFramesToNotify) {
- maybeNotifyDroppedFrames();
- }
- }
-
- private void skipBuffer() {
- decoderCounters.skippedOutputBufferCount++;
- outputBuffer.release();
- outputBuffer = null;
- }
-
- private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
- if (bitmap == null || bitmap.getWidth() != outputBuffer.width
- || bitmap.getHeight() != outputBuffer.height) {
- bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
- }
- bitmap.copyPixelsFromBuffer(outputBuffer.data);
- Canvas canvas = surface.lockCanvas(null);
- if (scale) {
- canvas.scale(((float) canvas.getWidth()) / outputBuffer.width,
- ((float) canvas.getHeight()) / outputBuffer.height);
- }
- canvas.drawBitmap(bitmap, 0, 0, null);
- surface.unlockCanvasAndPost(canvas);
- }
-
- private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
- if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
- || inputStreamEnded) {
- // We need to reinitialize the decoder or the input stream has ended.
- return false;
- }
-
- if (inputBuffer == null) {
- inputBuffer = decoder.dequeueInputBuffer();
- if (inputBuffer == null) {
- return false;
- }
- }
-
- if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
- inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
- decoder.queueInputBuffer(inputBuffer);
- inputBuffer = null;
- decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
- return false;
- }
-
- int result;
- if (waitingForKeys) {
- // We've already read an encrypted sample into buffer, and are waiting for keys.
- result = C.RESULT_BUFFER_READ;
- } else {
- result = readSource(formatHolder, inputBuffer, false);
- }
-
- if (result == C.RESULT_NOTHING_READ) {
- return false;
- }
- if (result == C.RESULT_FORMAT_READ) {
- onInputFormatChanged(formatHolder.format);
- return true;
- }
- if (inputBuffer.isEndOfStream()) {
- inputStreamEnded = true;
- decoder.queueInputBuffer(inputBuffer);
- inputBuffer = null;
- return false;
- }
- boolean bufferEncrypted = inputBuffer.isEncrypted();
- waitingForKeys = shouldWaitForKeys(bufferEncrypted);
- if (waitingForKeys) {
- return false;
- }
- inputBuffer.flip();
- inputBuffer.colorInfo = formatHolder.format.colorInfo;
- decoder.queueInputBuffer(inputBuffer);
- buffersInCodecCount++;
- decoderReceivedBuffers = true;
- decoderCounters.inputBufferCount++;
- inputBuffer = null;
- return true;
- }
-
- private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
- if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
- return false;
- }
- @DrmSession.State int drmSessionState = drmSession.getState();
- if (drmSessionState == DrmSession.STATE_ERROR) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
- }
- return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
- }
-
- private void flushDecoder() throws ExoPlaybackException {
- waitingForKeys = false;
- forceRenderFrame = false;
- buffersInCodecCount = 0;
- if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
- releaseDecoder();
- maybeInitDecoder();
- } else {
- inputBuffer = null;
- if (outputBuffer != null) {
- outputBuffer.release();
- outputBuffer = null;
- }
- if (nextOutputBuffer != null) {
- nextOutputBuffer.release();
- nextOutputBuffer = null;
- }
- decoder.flush();
- decoderReceivedBuffers = false;
- }
- }
@Override
public boolean isEnded() {
@@ -602,42 +355,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- private void maybeInitDecoder() throws ExoPlaybackException {
- if (decoder != null) {
- return;
- }
+ /**
+ * Called when a decoder has been created and configured.
+ *
+ * The default implementation is a no-op.
+ *
+ * @param name The name of the decoder that was initialized.
+ * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
+ * finished.
+ * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds.
+ */
+ @CallSuper
+ protected void onDecoderInitialized(
+ String name, long initializedTimestampMs, long initializationDurationMs) {
+ eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
+ }
- drmSession = pendingDrmSession;
- ExoMediaCrypto mediaCrypto = null;
- if (drmSession != null) {
- mediaCrypto = drmSession.getMediaCrypto();
- if (mediaCrypto == null) {
- DrmSessionException drmError = drmSession.getError();
- if (drmError != null) {
- throw ExoPlaybackException.createForRenderer(drmError, getIndex());
- }
- // The drm session isn't open yet.
- return;
+ /**
+ * Flushes the decoder.
+ *
+ * @throws ExoPlaybackException If an error occurs reinitializing a decoder.
+ */
+ @CallSuper
+ protected void flushDecoder() throws ExoPlaybackException {
+ waitingForKeys = false;
+ forceRenderFrame = false;
+ buffersInCodecCount = 0;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
}
- }
-
- try {
- long codecInitializingTimestamp = SystemClock.elapsedRealtime();
- TraceUtil.beginSection("createVpxDecoder");
- decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
- mediaCrypto);
- decoder.setOutputMode(outputMode);
- TraceUtil.endSection();
- long codecInitializedTimestamp = SystemClock.elapsedRealtime();
- eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
- codecInitializedTimestamp - codecInitializingTimestamp);
- decoderCounters.decoderInitCount++;
- } catch (VpxDecoderException e) {
- throw ExoPlaybackException.createForRenderer(e, getIndex());
+ if (nextOutputBuffer != null) {
+ nextOutputBuffer.release();
+ nextOutputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
}
}
- private void releaseDecoder() {
+ /** Releases the decoder. */
+ @CallSuper
+ protected void releaseDecoder() {
if (decoder == null) {
return;
}
@@ -654,7 +418,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
buffersInCodecCount = 0;
}
- private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
+ /**
+ * Called when a new format is read from the upstream source.
+ *
+ * @param newFormat The new format.
+ * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder.
+ */
+ @CallSuper
+ protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = format;
format = newFormat;
@@ -689,6 +460,147 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
eventDispatcher.inputFormatChanged(format);
}
+ /**
+ * Called immediately before an input buffer is queued into the decoder.
+ *
+ *
The default implementation is a no-op.
+ *
+ * @param buffer The buffer that will be queued.
+ */
+ protected void onQueueInputBuffer(VpxInputBuffer buffer) {
+ // Do nothing.
+ }
+
+ /**
+ * Called when an output buffer is successfully processed.
+ *
+ * @param presentationTimeUs The timestamp associated with the output buffer.
+ */
+ @CallSuper
+ protected void onProcessedOutputBuffer(long presentationTimeUs) {
+ buffersInCodecCount--;
+ }
+
+ /**
+ * Returns whether the current frame should be dropped.
+ *
+ * @param outputBufferTimeUs The timestamp of the current output buffer.
+ * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#TIME_UNSET}
+ * if the next output buffer is unavailable.
+ * @param positionUs The current playback position.
+ * @param joiningDeadlineMs The joining deadline.
+ * @return Returns whether to drop the current output buffer.
+ */
+ protected boolean shouldDropOutputBuffer(
+ long outputBufferTimeUs,
+ long nextOutputBufferTimeUs,
+ long positionUs,
+ long joiningDeadlineMs) {
+ return isBufferLate(outputBufferTimeUs - positionUs)
+ && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET);
+ }
+
+ /**
+ * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after
+ * the current playback position, if possible.
+ *
+ * @param earlyUs The time until the current buffer should be presented in microseconds. A
+ * negative value indicates that the buffer is late.
+ */
+ protected boolean shouldDropBuffersToKeyframe(long earlyUs) {
+ return isBufferVeryLate(earlyUs);
+ }
+
+ /**
+ * Skips the specified output buffer and releases it.
+ *
+ * @param outputBuffer The output buffer to skip.
+ */
+ protected void skipOutputBuffer(VpxOutputBuffer outputBuffer) {
+ decoderCounters.skippedOutputBufferCount++;
+ outputBuffer.release();
+ }
+
+ /**
+ * Drops the specified output buffer and releases it.
+ *
+ * @param outputBuffer The output buffer to drop.
+ */
+ protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) {
+ updateDroppedBufferCounters(1);
+ outputBuffer.release();
+ }
+
+ /**
+ * Renders the specified output buffer.
+ *
+ *
The implementation of this method takes ownership of the output buffer and is responsible
+ * for calling {@link VpxOutputBuffer#release()} either immediately or in the future.
+ *
+ * @param outputBuffer The buffer to render.
+ */
+ protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) {
+ int bufferMode = outputBuffer.mode;
+ boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null;
+ boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null;
+ if (!renderRgb && !renderYuv) {
+ dropOutputBuffer(outputBuffer);
+ } else {
+ maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height);
+ if (renderRgb) {
+ renderRgbFrame(outputBuffer, scaleToFit);
+ outputBuffer.release();
+ } else /* renderYuv */ {
+ outputBufferRenderer.setOutputBuffer(outputBuffer);
+ // The renderer will release the buffer.
+ }
+ consecutiveDroppedFrameCount = 0;
+ decoderCounters.renderedOutputBufferCount++;
+ maybeNotifyRenderedFirstFrame();
+ }
+ }
+
+ /**
+ * Drops frames from the current output buffer to the next keyframe at or before the playback
+ * position. If no such keyframe exists, as the playback position is inside the same group of
+ * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise.
+ *
+ * @param positionUs The current playback position, in microseconds.
+ * @return Whether any buffers were dropped.
+ * @throws ExoPlaybackException If an error occurs flushing the decoder.
+ */
+ protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException {
+ int droppedSourceBufferCount = skipSource(positionUs);
+ if (droppedSourceBufferCount == 0) {
+ return false;
+ }
+ decoderCounters.droppedToKeyframeCount++;
+ // We dropped some buffers to catch up, so update the decoder counters and flush the decoder,
+ // which releases all pending buffers buffers including the current output buffer.
+ updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount);
+ flushDecoder();
+ return true;
+ }
+
+ /**
+ * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were
+ * dropped.
+ *
+ * @param droppedBufferCount The number of additional dropped buffers.
+ */
+ protected void updateDroppedBufferCounters(int droppedBufferCount) {
+ decoderCounters.droppedBufferCount += droppedBufferCount;
+ droppedFrames += droppedBufferCount;
+ consecutiveDroppedFrameCount += droppedBufferCount;
+ decoderCounters.maxConsecutiveDroppedBufferCount =
+ Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount);
+ if (droppedFrames >= maxDroppedFramesToNotify) {
+ maybeNotifyDroppedFrames();
+ }
+ }
+
+ // PlayerMessage.Target implementation.
+
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
@@ -700,7 +612,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
- private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) {
+ // Internal methods.
+
+ private void setOutput(
+ @Nullable Surface surface, @Nullable VpxOutputBufferRenderer outputBufferRenderer) {
// At most one output may be non-null. Both may be null if the output is being cleared.
Assertions.checkState(surface == null || outputBufferRenderer == null);
if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) {
@@ -734,6 +649,240 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
+ private void maybeInitDecoder() throws ExoPlaybackException {
+ if (decoder != null) {
+ return;
+ }
+
+ drmSession = pendingDrmSession;
+ ExoMediaCrypto mediaCrypto = null;
+ if (drmSession != null) {
+ mediaCrypto = drmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = drmSession.getError();
+ if (drmError != null) {
+ // Continue for now. We may be able to avoid failure if the session recovers, or if a new
+ // input format causes the session to be replaced before it's used.
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
+ }
+ }
+
+ try {
+ long decoderInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createVpxDecoder");
+ decoder =
+ new VpxDecoder(
+ NUM_INPUT_BUFFERS,
+ NUM_OUTPUT_BUFFERS,
+ INITIAL_INPUT_BUFFER_SIZE,
+ mediaCrypto,
+ disableLoopFilter);
+ decoder.setOutputMode(outputMode);
+ TraceUtil.endSection();
+ long decoderInitializedTimestamp = SystemClock.elapsedRealtime();
+ onDecoderInitialized(
+ decoder.getName(),
+ decoderInitializedTimestamp,
+ decoderInitializedTimestamp - decoderInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (VpxDecoderException e) {
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
+ }
+ }
+
+ private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
+ if (decoder == null
+ || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the decoder or the input stream has ended.
+ return false;
+ }
+
+ if (inputBuffer == null) {
+ inputBuffer = decoder.dequeueInputBuffer();
+ if (inputBuffer == null) {
+ return false;
+ }
+ }
+
+ if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
+ int result;
+ if (waitingForKeys) {
+ // We've already read an encrypted sample into buffer, and are waiting for keys.
+ result = C.RESULT_BUFFER_READ;
+ } else {
+ result = readSource(formatHolder, inputBuffer, false);
+ }
+
+ if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder.format);
+ return true;
+ }
+ if (inputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ return false;
+ }
+ boolean bufferEncrypted = inputBuffer.isEncrypted();
+ waitingForKeys = shouldWaitForKeys(bufferEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ inputBuffer.flip();
+ inputBuffer.colorInfo = formatHolder.format.colorInfo;
+ onQueueInputBuffer(inputBuffer);
+ decoder.queueInputBuffer(inputBuffer);
+ buffersInCodecCount++;
+ decoderReceivedBuffers = true;
+ decoderCounters.inputBufferCount++;
+ inputBuffer = null;
+ return true;
+ }
+
+ /**
+ * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link
+ * #processOutputBuffer(long)}.
+ *
+ * @param positionUs The player's current position.
+ * @return Whether it may be possible to drain more output data.
+ * @throws ExoPlaybackException If an error occurs draining the output buffer.
+ */
+ private boolean drainOutputBuffer(long positionUs)
+ throws ExoPlaybackException, VpxDecoderException {
+ // Acquire outputBuffer either from nextOutputBuffer or from the decoder.
+ if (outputBuffer == null) {
+ if (nextOutputBuffer != null) {
+ outputBuffer = nextOutputBuffer;
+ nextOutputBuffer = null;
+ } else {
+ outputBuffer = decoder.dequeueOutputBuffer();
+ }
+ if (outputBuffer == null) {
+ return false;
+ }
+ decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ buffersInCodecCount -= outputBuffer.skippedOutputBufferCount;
+ }
+
+ if (nextOutputBuffer == null) {
+ nextOutputBuffer = decoder.dequeueOutputBuffer();
+ }
+
+ if (outputBuffer.isEndOfStream()) {
+ if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the decoder, and have now processed all final buffers.
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ outputStreamEnded = true;
+ }
+ return false;
+ }
+
+ return processOutputBuffer(positionUs);
+ }
+
+ /**
+ * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns
+ * whether it may be possible to process another output buffer.
+ *
+ * @param positionUs The player's current position.
+ * @return Whether it may be possible to drain another output buffer.
+ * @throws ExoPlaybackException If an error occurs processing the output buffer.
+ */
+ private boolean processOutputBuffer(long positionUs) throws ExoPlaybackException {
+ if (outputMode == VpxDecoder.OUTPUT_MODE_NONE) {
+ // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
+ if (isBufferLate(outputBuffer.timeUs - positionUs)) {
+ forceRenderFrame = false;
+ skipOutputBuffer(outputBuffer);
+ onProcessedOutputBuffer(outputBuffer.timeUs);
+ outputBuffer = null;
+ return true;
+ }
+ return false;
+ }
+
+ if (forceRenderFrame) {
+ forceRenderFrame = false;
+ renderOutputBuffer(outputBuffer);
+ onProcessedOutputBuffer(outputBuffer.timeUs);
+ outputBuffer = null;
+ return true;
+ }
+
+ long nextOutputBufferTimeUs =
+ nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream()
+ ? nextOutputBuffer.timeUs
+ : C.TIME_UNSET;
+
+ long earlyUs = outputBuffer.timeUs - positionUs;
+ if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) {
+ forceRenderFrame = true;
+ return false;
+ } else if (shouldDropOutputBuffer(
+ outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) {
+ dropOutputBuffer(outputBuffer);
+ onProcessedOutputBuffer(outputBuffer.timeUs);
+ outputBuffer = null;
+ return true;
+ }
+
+ // If we have yet to render a frame to the current output (either initially or immediately
+ // following a seek), render one irrespective of the state or current position.
+ if (!renderedFirstFrame || (getState() == STATE_STARTED && earlyUs <= 30000)) {
+ renderOutputBuffer(outputBuffer);
+ onProcessedOutputBuffer(outputBuffer.timeUs);
+ outputBuffer = null;
+ }
+
+ return false;
+ }
+
+ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+ if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
+ return false;
+ }
+ @DrmSession.State int drmSessionState = drmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ }
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
+ }
+
+ private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) {
+ if (bitmap == null
+ || bitmap.getWidth() != outputBuffer.width
+ || bitmap.getHeight() != outputBuffer.height) {
+ bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565);
+ }
+ bitmap.copyPixelsFromBuffer(outputBuffer.data);
+ Canvas canvas = surface.lockCanvas(null);
+ if (scale) {
+ canvas.scale(
+ ((float) canvas.getWidth()) / outputBuffer.width,
+ ((float) canvas.getHeight()) / outputBuffer.height);
+ }
+ canvas.drawBitmap(bitmap, 0, 0, null);
+ surface.unlockCanvasAndPost(canvas);
+ }
+
private void setJoiningDeadlineMs() {
joiningDeadlineMs = allowedJoiningTimeMs > 0
? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET;
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
index ef999d5d2b..6f8c0a1918 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java
@@ -49,10 +49,11 @@ import java.nio.ByteBuffer;
* @param initialInputBufferSize The initial size of each input buffer.
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
+ * @param disableLoopFilter Disable the libvpx in-loop smoothing filter.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
- ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
+ ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException {
super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
@@ -61,7 +62,7 @@ import java.nio.ByteBuffer;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
- vpxDecContext = vpxInit();
+ vpxDecContext = vpxInit(disableLoopFilter);
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
}
@@ -98,6 +99,11 @@ import java.nio.ByteBuffer;
super.releaseOutputBuffer(buffer);
}
+ @Override
+ protected VpxDecoderException createUnexpectedDecodeException(Throwable error) {
+ return new VpxDecoderException("Unexpected decode error", error);
+ }
+
@Override
protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer,
boolean reset) {
@@ -139,7 +145,7 @@ import java.nio.ByteBuffer;
vpxClose(vpxDecContext);
}
- private native long vpxInit();
+ private native long vpxInit(boolean disableLoopFilter);
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
index 5f43b503ac..8de14629d3 100644
--- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
+++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java
@@ -15,10 +15,8 @@
*/
package com.google.android.exoplayer2.ext.vp9;
-/**
- * Thrown when a libvpx decoder error occurs.
- */
-public class VpxDecoderException extends Exception {
+/** Thrown when a libvpx decoder error occurs. */
+public final class VpxDecoderException extends Exception {
/* package */ VpxDecoderException(String message) {
super(message);
diff --git a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
index 5f058d0551..eab6862555 100755
--- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
+++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh
@@ -102,7 +102,10 @@ for i in $(seq 0 ${limit}); do
# configure and make
echo "build_android_configs: "
echo "configure ${config[${i}]} ${common_params}"
- ../../libvpx/configure ${config[${i}]} ${common_params}
+ ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \
+ -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \
+ -isystem $ndk/sysroot/usr/include \
+ "
rm -f libvpx_srcs.txt
for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc
index 5c480d1525..421b16d26d 100644
--- a/extensions/vp9/src/main/jni/vpx_jni.cc
+++ b/extensions/vp9/src/main/jni/vpx_jni.cc
@@ -218,7 +218,6 @@ static int convert_16_to_8_neon(const vpx_image_t* const img, jbyte* const data,
dstV += 8;
}
- i *= 4;
uint32_t randval = 0;
while (i < uvWidth) {
if (!randval) randval = random();
@@ -283,7 +282,7 @@ static void convert_16_to_8_standard(const vpx_image_t* const img,
}
}
-DECODER_FUNC(jlong, vpxInit) {
+DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) {
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount();
@@ -295,6 +294,9 @@ DECODER_FUNC(jlong, vpxInit) {
errorCode = err;
return 0;
}
+ if (disableLoopFilter) {
+ vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true);
+ }
// Populate JNI References.
const jclass outputBufferClass = env->FindClass(
@@ -360,7 +362,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
// resize buffer if required.
jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
img->d_w, img->d_h);
- if (initResult == JNI_FALSE) {
+ if (env->ExceptionCheck() || !initResult) {
return -1;
}
@@ -398,7 +400,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
jboolean initResult = env->CallBooleanMethod(
jOutputBuffer, initForYuvFrame, img->d_w, img->d_h,
img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], colorspace);
- if (initResult == JNI_FALSE) {
+ if (env->ExceptionCheck() || !initResult) {
return -1;
}
diff --git a/library/core/build.gradle b/library/core/build.gradle
index d50834efd5..a87e11065f 100644
--- a/library/core/build.gradle
+++ b/library/core/build.gradle
@@ -21,6 +21,7 @@ android {
defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
+ consumerProguardFiles 'proguard-rules.txt'
}
// Workaround to prevent circular dependency on project :testutils.
@@ -46,6 +47,7 @@ dependencies {
compile 'com.android.support:support-annotations:' + supportLibraryVersion
androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion
+ androidTestCompile 'com.google.truth:truth:' + truthVersion
androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion
testCompile 'com.google.truth:truth:' + truthVersion
testCompile 'junit:junit:' + junitVersion
diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt
new file mode 100644
index 0000000000..7dc81c3f73
--- /dev/null
+++ b/library/core/proguard-rules.txt
@@ -0,0 +1,31 @@
+# Proguard rules specific to the core module.
+
+# Constructors accessed via reflection in DefaultRenderersFactory
+-dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer
+-keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer {
+ (boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int);
+}
+-dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer
+-keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer {
+ (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);
+}
+-dontnote com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer
+-keepclassmembers class com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer {
+ (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);
+}
+-dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer
+-keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer {
+ (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioProcessor[]);
+}
+
+# Constructors accessed via reflection in DefaultExtractorsFactory
+-dontnote com.google.android.exoplayer2.ext.flac.FlacExtractor
+-keepclassmembers class com.google.android.exoplayer2.ext.flac.FlacExtractor {
+ ();
+}
+
+# Constructors accessed via reflection in DefaultDataSource
+-dontnote com.google.android.exoplayer2.ext.rtmp.RtmpDataSource
+-keepclassmembers class com.google.android.exoplayer2.ext.rtmp.RtmpDataSource {
+ ();
+}
diff --git a/library/core/src/androidTest/assets/ssa/typical_format b/library/core/src/androidTest/assets/ssa/typical_format
deleted file mode 100644
index 0cc5f1690f..0000000000
--- a/library/core/src/androidTest/assets/ssa/typical_format
+++ /dev/null
@@ -1 +0,0 @@
-Format: Layer, Start, End, Style, Name, Text
\ No newline at end of file
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
deleted file mode 100644
index 95d5d96163..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java
+++ /dev/null
@@ -1,437 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
-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.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.FakeShuffleOrder;
-import com.google.android.exoplayer2.testutil.FakeTimeline;
-import com.google.android.exoplayer2.testutil.FakeTrackSelection;
-import com.google.android.exoplayer2.testutil.FakeTrackSelector;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import junit.framework.TestCase;
-
-/**
- * Unit test for {@link ExoPlayer}.
- */
-public final class ExoPlayerTest extends TestCase {
-
- /**
- * For tests that rely on the player transitioning to the ended state, the duration in
- * milliseconds after starting the player before the test will time out. This is to catch cases
- * where the player under test is not making progress, in which case the test should fail.
- */
- private static final int TIMEOUT_MS = 10000;
-
- /**
- * Tests playback of a source that exposes an empty timeline. Playback is expected to end without
- * error.
- */
- public void testPlayEmptyTimeline() throws Exception {
- Timeline timeline = Timeline.EMPTY;
- FakeRenderer renderer = new FakeRenderer();
- ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
- .setTimeline(timeline).setRenderers(renderer)
- .build().start().blockUntilEnded(TIMEOUT_MS);
- testRunner.assertPositionDiscontinuityCount(0);
- testRunner.assertTimelinesEqual();
- assertEquals(0, renderer.formatReadCount);
- assertEquals(0, renderer.bufferReadCount);
- assertFalse(renderer.isEnded);
- }
-
- /**
- * Tests playback of a source that exposes a single period.
- */
- public void testPlaySinglePeriodTimeline() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
- Object manifest = new Object();
- 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);
- }
-
- /**
- * Tests playback of a source that exposes three periods.
- */
- public void testPlayMultiPeriodTimeline() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
- 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);
- }
-
- /**
- * Tests that the player does not unnecessarily reset renderers when playing a multi-period
- * source.
- */
- public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
- final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) {
-
- @Override
- public long getPositionUs() {
- // Simulate the playback position lagging behind the reading position: the renderer media
- // clock position will be the start of the timeline until the stream is set to be final, at
- // which point it jumps to the end of the timeline allowing the playing period to advance.
- // TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US.
- return isCurrentStreamFinal() ? 60000030 : 60000000;
- }
-
- @Override
- public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
- return PlaybackParameters.DEFAULT;
- }
-
- @Override
- public PlaybackParameters getPlaybackParameters() {
- return PlaybackParameters.DEFAULT;
- }
-
- @Override
- public boolean isEnded() {
- return videoRenderer.isEnded();
- }
-
- };
- 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);
- }
-
- public void testRepreparationGivesFreshSourceInfo() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
- FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- Object firstSourceManifest = new Object();
- MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest,
- Builder.VIDEO_FORMAT);
- final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1);
- final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1);
- 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);
- // We've queued a source info refresh on the playback thread's event queue. Allow the test
- // thread to prepare the player with the third source, and block this thread (the playback
- // thread) until the test thread's call to prepare() has returned.
- queuedSourceInfoCountDownLatch.countDown();
- try {
- completePreparationCountDownLatch.await();
- } catch (InterruptedException e) {
- throw new IllegalStateException(e);
- }
- }
- };
- Object thirdSourceManifest = new Object();
- 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.
- 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 {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 3);
- 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);
- }
-
- public void testShuffleModeEnabledChanges() throws Exception {
- Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1);
- MediaSource[] fakeMediaSources = {
- new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
- new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT),
- new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT)
- };
- ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false,
- new FakeShuffleOrder(3), fakeMediaSources);
- FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- ActionSchedule actionSchedule = new ActionSchedule.Builder("testShuffleModeEnabled")
- .setRepeatMode(Player.REPEAT_MODE_ALL).waitForPositionDiscontinuity() // 0 -> 1
- .setShuffleModeEnabled(true).waitForPositionDiscontinuity() // 1 -> 0
- .waitForPositionDiscontinuity().waitForPositionDiscontinuity() // 0 -> 2 -> 1
- .setShuffleModeEnabled(false).setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 -> end
- .build();
- ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder()
- .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule)
- .build().start().blockUntilEnded(TIMEOUT_MS);
- testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2);
- assertTrue(renderer.isEnded);
- }
-
- public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception {
- FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased")
- .setRepeatMode(Player.REPEAT_MODE_ALL)
- .waitForPositionDiscontinuity()
- .seek(0) // Seek with repeat mode set to REPEAT_MODE_ALL.
- .waitForPositionDiscontinuity()
- .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish.
- .build();
- new ExoPlayerTestRunner.Builder()
- .setRenderers(renderer).setActionSchedule(actionSchedule)
- .build().start().blockUntilEnded(TIMEOUT_MS);
- assertTrue(renderer.isEnded);
- }
-
- public void testSeekProcessedCallback() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
- ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback")
- // Initial seek before timeline preparation finished.
- .pause().seek(10).waitForPlaybackState(Player.STATE_READY)
- // Re-seek to same position, start playback and wait until playback reaches second window.
- .seek(10).play().waitForPositionDiscontinuity()
- // Seek twice in concession, expecting the first seek to be replaced.
- .seek(5).seek(60).build();
- final List playbackStatesWhenSeekProcessed = new ArrayList<>();
- Player.EventListener eventListener = new Player.DefaultEventListener() {
- private int currentPlaybackState = Player.STATE_IDLE;
-
- @Override
- public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
- currentPlaybackState = playbackState;
- }
-
- @Override
- public void onSeekProcessed() {
- playbackStatesWhenSeekProcessed.add(currentPlaybackState);
- }
- };
- new ExoPlayerTestRunner.Builder()
- .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule)
- .build().start().blockUntilEnded(TIMEOUT_MS);
- assertEquals(3, playbackStatesWhenSeekProcessed.size());
- assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0));
- assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1));
- assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2));
- }
-
- public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
- MediaSource mediaSource =
- new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
- FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
- FakeTrackSelector trackSelector = new FakeTrackSelector();
-
- new ExoPlayerTestRunner.Builder()
- .setMediaSource(mediaSource)
- .setRenderers(videoRenderer, audioRenderer)
- .setTrackSelector(trackSelector)
- .build().start().blockUntilEnded(TIMEOUT_MS);
-
- List createdTrackSelections = trackSelector.getSelectedTrackSelections();
- int numSelectionsEnabled = 0;
- // Assert that all tracks selection are disabled at the end of the playback.
- for (FakeTrackSelection trackSelection : createdTrackSelections) {
- assertFalse(trackSelection.isEnabled);
- numSelectionsEnabled += trackSelection.enableCount;
- }
- // There are 2 renderers, and track selections are made once (1 period).
- // Track selections are not reused, so there are 2 track selections made.
- assertEquals(2, createdTrackSelections.size());
- // There should be 2 track selections enabled in total.
- assertEquals(2, numSelectionsEnabled);
- }
-
- public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 2);
- MediaSource mediaSource =
- new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
- FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
- FakeTrackSelector trackSelector = new FakeTrackSelector();
-
- new ExoPlayerTestRunner.Builder()
- .setMediaSource(mediaSource)
- .setRenderers(videoRenderer, audioRenderer)
- .setTrackSelector(trackSelector)
- .build().start().blockUntilEnded(TIMEOUT_MS);
-
- List createdTrackSelections = trackSelector.getSelectedTrackSelections();
- int numSelectionsEnabled = 0;
- // Assert that all tracks selection are disabled at the end of the playback.
- for (FakeTrackSelection trackSelection : createdTrackSelections) {
- assertFalse(trackSelection.isEnabled);
- numSelectionsEnabled += trackSelection.enableCount;
- }
- // There are 2 renderers, and track selections are made twice (2 periods).
- // Track selections are not reused, so there are 4 track selections made.
- assertEquals(4, createdTrackSelections.size());
- // There should be 4 track selections enabled in total.
- assertEquals(4, numSelectionsEnabled);
- }
-
- public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade()
- throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
- MediaSource mediaSource =
- new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
- FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
- final FakeTrackSelector trackSelector = new FakeTrackSelector();
- ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection")
- .waitForPlaybackState(Player.STATE_READY)
- .executeRunnable(new Runnable() {
- @Override
- public void run() {
- trackSelector.setRendererDisabled(0, true);
- }
- }).build();
-
- new ExoPlayerTestRunner.Builder()
- .setMediaSource(mediaSource)
- .setRenderers(videoRenderer, audioRenderer)
- .setTrackSelector(trackSelector)
- .setActionSchedule(disableTrackAction)
- .build().start().blockUntilEnded(TIMEOUT_MS);
-
- List createdTrackSelections = trackSelector.getSelectedTrackSelections();
- int numSelectionsEnabled = 0;
- // Assert that all tracks selection are disabled at the end of the playback.
- for (FakeTrackSelection trackSelection : createdTrackSelections) {
- assertFalse(trackSelection.isEnabled);
- numSelectionsEnabled += trackSelection.enableCount;
- }
- // There are 2 renderers, and track selections are made twice.
- // Track selections are not reused, so there are 4 track selections made.
- assertEquals(4, createdTrackSelections.size());
- // Initially there are 2 track selections enabled.
- // The second time one renderer is disabled, so only 1 track selection should be enabled.
- assertEquals(3, numSelectionsEnabled);
- }
-
- public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed()
- throws Exception {
- Timeline timeline = new FakeTimeline(/* windowCount= */ 1);
- MediaSource mediaSource =
- new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT);
- FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT);
- FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT);
- final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true);
- ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection")
- .waitForPlaybackState(Player.STATE_READY)
- .executeRunnable(new Runnable() {
- @Override
- public void run() {
- trackSelector.setRendererDisabled(0, true);
- }
- }).build();
-
- new ExoPlayerTestRunner.Builder()
- .setMediaSource(mediaSource)
- .setRenderers(videoRenderer, audioRenderer)
- .setTrackSelector(trackSelector)
- .setActionSchedule(disableTrackAction)
- .build().start().blockUntilEnded(TIMEOUT_MS);
-
- List createdTrackSelections = trackSelector.getSelectedTrackSelections();
- int numSelectionsEnabled = 0;
- // Assert that all tracks selection are disabled at the end of the playback.
- for (FakeTrackSelection trackSelection : createdTrackSelections) {
- assertFalse(trackSelection.isEnabled);
- numSelectionsEnabled += trackSelection.enableCount;
- }
- // There are 2 renderers, and track selections are made twice.
- // TrackSelections are reused, so there are only 2 track selections made for 2 renderers.
- assertEquals(2, createdTrackSelections.size());
- // Initially there are 2 track selections enabled.
- // The second time one renderer is disabled, so only 1 track selection should be enabled.
- assertEquals(3, numSelectionsEnabled);
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java
deleted file mode 100644
index 186b842bab..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPacketTest.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright (C) 2016 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.ogg;
-
-import android.test.InstrumentationTestCase;
-import android.test.MoreAsserts;
-import com.google.android.exoplayer2.testutil.FakeExtractorInput;
-import com.google.android.exoplayer2.testutil.OggTestData;
-import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.util.ParsableByteArray;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Random;
-
-/**
- * Unit test for {@link OggPacket}.
- */
-public final class OggPacketTest extends InstrumentationTestCase {
-
- private static final String TEST_FILE = "ogg/bear.opus";
-
- private Random random;
- private OggPacket oggPacket;
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- random = new Random(0);
- oggPacket = new OggPacket();
- }
-
- public void testReadPacketsWithEmptyPage() throws Exception {
- byte[] firstPacket = TestUtil.buildTestData(8, random);
- byte[] secondPacket = TestUtil.buildTestData(272, random);
- byte[] thirdPacket = TestUtil.buildTestData(256, random);
- byte[] fourthPacket = TestUtil.buildTestData(271, random);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- // First page with a single packet.
- OggTestData.buildOggHeader(0x02, 0, 1000, 0x01),
- TestUtil.createByteArray(0x08), // Laces
- firstPacket,
- // Second page with a single packet.
- OggTestData.buildOggHeader(0x00, 16, 1001, 0x02),
- TestUtil.createByteArray(0xFF, 0x11), // Laces
- secondPacket,
- // Third page with zero packets.
- OggTestData.buildOggHeader(0x00, 16, 1002, 0x00),
- // Fourth page with two packets.
- OggTestData.buildOggHeader(0x04, 128, 1003, 0x04),
- TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces
- thirdPacket,
- fourthPacket), true);
-
- assertReadPacket(input, firstPacket);
- assertTrue((oggPacket.getPageHeader().type & 0x02) == 0x02);
- assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04);
- assertEquals(0x02, oggPacket.getPageHeader().type);
- assertEquals(27 + 1, oggPacket.getPageHeader().headerSize);
- assertEquals(8, oggPacket.getPageHeader().bodySize);
- assertEquals(0x00, oggPacket.getPageHeader().revision);
- assertEquals(1, oggPacket.getPageHeader().pageSegmentCount);
- assertEquals(1000, oggPacket.getPageHeader().pageSequenceNumber);
- assertEquals(4096, oggPacket.getPageHeader().streamSerialNumber);
- assertEquals(0, oggPacket.getPageHeader().granulePosition);
-
- assertReadPacket(input, secondPacket);
- assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
- assertFalse((oggPacket.getPageHeader().type & 0x04) == 0x04);
- assertEquals(0, oggPacket.getPageHeader().type);
- assertEquals(27 + 2, oggPacket.getPageHeader().headerSize);
- assertEquals(255 + 17, oggPacket.getPageHeader().bodySize);
- assertEquals(2, oggPacket.getPageHeader().pageSegmentCount);
- assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber);
- assertEquals(16, oggPacket.getPageHeader().granulePosition);
-
- assertReadPacket(input, thirdPacket);
- assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
- assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
- assertEquals(4, oggPacket.getPageHeader().type);
- assertEquals(27 + 4, oggPacket.getPageHeader().headerSize);
- assertEquals(255 + 1 + 255 + 16, oggPacket.getPageHeader().bodySize);
- assertEquals(4, oggPacket.getPageHeader().pageSegmentCount);
- // Page 1002 is empty, so current page is 1003.
- assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber);
- assertEquals(128, oggPacket.getPageHeader().granulePosition);
-
- assertReadPacket(input, fourthPacket);
-
- assertReadEof(input);
- }
-
- public void testReadPacketWithZeroSizeTerminator() throws Exception {
- byte[] firstPacket = TestUtil.buildTestData(255, random);
- byte[] secondPacket = TestUtil.buildTestData(8, random);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- OggTestData.buildOggHeader(0x06, 0, 1000, 0x04),
- TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces.
- firstPacket,
- secondPacket), true);
-
- assertReadPacket(input, firstPacket);
- assertReadPacket(input, secondPacket);
- assertReadEof(input);
- }
-
- public void testReadContinuedPacketOverTwoPages() throws Exception {
- byte[] firstPacket = TestUtil.buildTestData(518);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- // First page.
- OggTestData.buildOggHeader(0x02, 0, 1000, 0x02),
- TestUtil.createByteArray(0xFF, 0xFF), // Laces.
- Arrays.copyOf(firstPacket, 510),
- // Second page (continued packet).
- OggTestData.buildOggHeader(0x05, 10, 1001, 0x01),
- TestUtil.createByteArray(0x08), // Laces.
- Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true);
-
- assertReadPacket(input, firstPacket);
- assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
- assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
- assertEquals(1001, oggPacket.getPageHeader().pageSequenceNumber);
-
- assertReadEof(input);
- }
-
- public void testReadContinuedPacketOverFourPages() throws Exception {
- byte[] firstPacket = TestUtil.buildTestData(1028);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- // First page.
- OggTestData.buildOggHeader(0x02, 0, 1000, 0x02),
- TestUtil.createByteArray(0xFF, 0xFF), // Laces.
- Arrays.copyOf(firstPacket, 510),
- // Second page (continued packet).
- OggTestData.buildOggHeader(0x01, 10, 1001, 0x01),
- TestUtil.createByteArray(0xFF), // Laces.
- Arrays.copyOfRange(firstPacket, 510, 510 + 255),
- // Third page (continued packet).
- OggTestData.buildOggHeader(0x01, 10, 1002, 0x01),
- TestUtil.createByteArray(0xFF), // Laces.
- Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255),
- // Fourth page (continued packet).
- OggTestData.buildOggHeader(0x05, 10, 1003, 0x01),
- TestUtil.createByteArray(0x08), // Laces.
- Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true);
-
- assertReadPacket(input, firstPacket);
- assertTrue((oggPacket.getPageHeader().type & 0x04) == 0x04);
- assertFalse((oggPacket.getPageHeader().type & 0x02) == 0x02);
- assertEquals(1003, oggPacket.getPageHeader().pageSequenceNumber);
-
- assertReadEof(input);
- }
-
- public void testReadDiscardContinuedPacketAtStart() throws Exception {
- byte[] pageBody = TestUtil.buildTestData(256 + 8);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- // Page with a continued packet at start.
- OggTestData.buildOggHeader(0x01, 10, 1001, 0x03),
- TestUtil.createByteArray(255, 1, 8), // Laces.
- pageBody), true);
-
- // Expect the first partial packet to be discarded.
- assertReadPacket(input, Arrays.copyOfRange(pageBody, 256, 256 + 8));
- assertReadEof(input);
- }
-
- public void testReadZeroSizedPacketsAtEndOfStream() throws Exception {
- byte[] firstPacket = TestUtil.buildTestData(8, random);
- byte[] secondPacket = TestUtil.buildTestData(8, random);
- byte[] thirdPacket = TestUtil.buildTestData(8, random);
-
- FakeExtractorInput input = OggTestData.createInput(
- TestUtil.joinByteArrays(
- OggTestData.buildOggHeader(0x02, 0, 1000, 0x01),
- TestUtil.createByteArray(0x08), // Laces.
- firstPacket,
- OggTestData.buildOggHeader(0x04, 0, 1001, 0x03),
- TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
- secondPacket,
- OggTestData.buildOggHeader(0x04, 0, 1002, 0x03),
- TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces.
- thirdPacket), true);
-
- assertReadPacket(input, firstPacket);
- assertReadPacket(input, secondPacket);
- assertReadPacket(input, thirdPacket);
- assertReadEof(input);
- }
-
-
- public void testParseRealFile() throws IOException, InterruptedException {
- byte[] data = TestUtil.getByteArray(getInstrumentation(), TEST_FILE);
- FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
- int packetCounter = 0;
- while (readPacket(input)) {
- packetCounter++;
- }
- assertEquals(277, packetCounter);
- }
-
- private void assertReadPacket(FakeExtractorInput extractorInput, byte[] expected)
- throws IOException, InterruptedException {
- assertTrue(readPacket(extractorInput));
- ParsableByteArray payload = oggPacket.getPayload();
- MoreAsserts.assertEquals(expected, Arrays.copyOf(payload.data, payload.limit()));
- }
-
- private void assertReadEof(FakeExtractorInput extractorInput)
- throws IOException, InterruptedException {
- assertFalse(readPacket(extractorInput));
- }
-
- private boolean readPacket(FakeExtractorInput input)
- throws InterruptedException, IOException {
- while (true) {
- try {
- return oggPacket.populate(input);
- } catch (FakeExtractorInput.SimulatedIOException e) {
- // Ignore.
- }
- }
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
deleted file mode 100644
index 3c870f06f4..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright (C) 2016 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 android.test.InstrumentationTestCase;
-import com.google.android.exoplayer2.C;
-import com.google.android.exoplayer2.Player;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.Timeline.Period;
-import com.google.android.exoplayer2.Timeline.Window;
-import com.google.android.exoplayer2.testutil.FakeMediaSource;
-import com.google.android.exoplayer2.testutil.FakeTimeline;
-import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
-import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
-import com.google.android.exoplayer2.testutil.TimelineAsserts;
-
-/**
- * Unit tests for {@link ClippingMediaSource}.
- */
-public final class ClippingMediaSourceTest extends InstrumentationTestCase {
-
- private static final long TEST_PERIOD_DURATION_US = 1000000;
- private static final long TEST_CLIP_AMOUNT_US = 300000;
-
- private Window window;
- private Period period;
-
- @Override
- protected void setUp() throws Exception {
- window = new Timeline.Window();
- period = new Timeline.Period();
- }
-
- public void testNoClipping() {
- Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
-
- Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
-
- assertEquals(1, clippedTimeline.getWindowCount());
- assertEquals(1, clippedTimeline.getPeriodCount());
- assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs());
- assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
- }
-
- public void testClippingUnseekableWindowThrows() {
- Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false);
-
- // If the unseekable window isn't clipped, clipping succeeds.
- getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
- try {
- // If the unseekable window is clipped, clipping fails.
- getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
- fail("Expected clipping to fail.");
- } catch (IllegalArgumentException e) {
- // Expected.
- }
- }
-
- public void testClippingStart() {
- Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
-
- Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
- TEST_PERIOD_DURATION_US);
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
- clippedTimeline.getWindow(0, window).getDurationUs());
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
- clippedTimeline.getPeriod(0, period).getDurationUs());
- }
-
- public void testClippingEnd() {
- Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
-
- Timeline clippedTimeline = getClippedTimeline(timeline, 0,
- TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
- clippedTimeline.getWindow(0, window).getDurationUs());
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
- clippedTimeline.getPeriod(0, period).getDurationUs());
- }
-
- public void testClippingStartAndEnd() {
- Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
-
- Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
- TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
- clippedTimeline.getWindow(0, window).getDurationUs());
- assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
- clippedTimeline.getPeriod(0, period).getDurationUs());
- }
-
- public void testWindowAndPeriodIndices() {
- Timeline timeline = new FakeTimeline(
- new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US));
- Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
- TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
- TimelineAsserts.assertWindowIds(clippedTimeline, 111);
- TimelineAsserts.assertPeriodCounts(clippedTimeline, 1);
- TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false,
- C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
- TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false,
- C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0);
- TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0);
- }
-
- /**
- * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
- */
- private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
- FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null);
- ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs);
- MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
- try {
- return testRunner.prepareSource();
- } finally {
- testRunner.release();
- }
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
deleted file mode 100644
index 1ca32be46d..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- * Copyright (C) 2016 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.Player;
-import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
-import com.google.android.exoplayer2.testutil.FakeMediaSource;
-import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
-import com.google.android.exoplayer2.testutil.FakeTimeline;
-import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition;
-import com.google.android.exoplayer2.testutil.MediaSourceTestRunner;
-import com.google.android.exoplayer2.testutil.TimelineAsserts;
-import junit.framework.TestCase;
-
-/**
- * Unit tests for {@link ConcatenatingMediaSource}.
- */
-public final class ConcatenatingMediaSourceTest extends TestCase {
-
- public void testEmptyConcatenation() {
- for (boolean atomic : new boolean[] {false, true}) {
- Timeline timeline = getConcatenatedTimeline(atomic);
- TimelineAsserts.assertEmpty(timeline);
-
- timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY);
- TimelineAsserts.assertEmpty(timeline);
-
- timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY);
- TimelineAsserts.assertEmpty(timeline);
- }
- }
-
- public void testSingleMediaSource() {
- Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111));
- TimelineAsserts.assertWindowIds(timeline, 111);
- TimelineAsserts.assertPeriodCounts(timeline, 3);
- for (boolean shuffled : new boolean[] {false, true}) {
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
- }
-
- timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111));
- TimelineAsserts.assertWindowIds(timeline, 111);
- TimelineAsserts.assertPeriodCounts(timeline, 3);
- for (boolean shuffled : new boolean[] {false, true}) {
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0);
- }
- }
-
- public void testMultipleMediaSources() {
- Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222),
- createFakeTimeline(3, 333) };
- Timeline timeline = getConcatenatedTimeline(false, timelines);
- TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
- TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);
- assertEquals(0, timeline.getFirstWindowIndex(false));
- assertEquals(2, timeline.getLastWindowIndex(false));
- assertEquals(2, timeline.getFirstWindowIndex(true));
- assertEquals(0, timeline.getLastWindowIndex(true));
-
- timeline = getConcatenatedTimeline(true, timelines);
- TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
- TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3);
- for (boolean shuffled : new boolean[] {false, true}) {
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled,
- 2, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled,
- 2, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);
- assertEquals(0, timeline.getFirstWindowIndex(shuffled));
- assertEquals(2, timeline.getLastWindowIndex(shuffled));
- }
- }
-
- public void testNestedMediaSources() {
- Timeline timeline = getConcatenatedTimeline(false,
- getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)),
- getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444)));
- TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444);
- TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- C.INDEX_UNSET, 0, 1, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false,
- 0, 1, 3, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false,
- 3, 0, 1, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- 1, 2, 3, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- 1, 3, C.INDEX_UNSET, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true,
- 0, 1, 3, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true,
- 1, 3, 0, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- C.INDEX_UNSET, 0, 3, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1);
- }
-
- public void testEmptyTimelineMediaSources() {
- // Empty timelines in the front, back, and the middle (single and multiple in a row).
- Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY,
- Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333),
- Timeline.EMPTY };
- Timeline timeline = getConcatenatedTimeline(false, timelines);
- TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
- TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1);
- assertEquals(0, timeline.getFirstWindowIndex(false));
- assertEquals(2, timeline.getLastWindowIndex(false));
- assertEquals(2, timeline.getFirstWindowIndex(true));
- assertEquals(0, timeline.getLastWindowIndex(true));
-
- timeline = getConcatenatedTimeline(true, timelines);
- TimelineAsserts.assertWindowIds(timeline, 111, 222, 333);
- TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3);
- for (boolean shuffled : new boolean[] {false, true}) {
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- C.INDEX_UNSET, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled,
- 2, 0, 1);
- TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled,
- 2, 0, 1);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled,
- 1, 2, C.INDEX_UNSET);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0);
- TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0);
- assertEquals(0, timeline.getFirstWindowIndex(shuffled));
- assertEquals(2, timeline.getLastWindowIndex(shuffled));
- }
- }
-
- public void testPeriodCreationWithAds() throws InterruptedException {
- // Create media source with ad child source.
- Timeline timelineContentOnly = new FakeTimeline(
- new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND));
- Timeline timelineWithAds = new FakeTimeline(
- new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1));
- FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null);
- FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null);
- ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly,
- mediaSourceWithAds);
-
- MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
- try {
- Timeline timeline = testRunner.prepareSource();
- TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1);
-
- // Create all periods and assert period creation of child media sources has been called.
- testRunner.assertPrepareAndReleaseAllPeriods();
- mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0));
- mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1));
- mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0));
- mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1));
- mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0));
- mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0));
- } finally {
- testRunner.release();
- }
- }
-
- /**
- * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns
- * the concatenated timeline.
- */
- private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic,
- Timeline... timelines) {
- MediaSource[] mediaSources = new MediaSource[timelines.length];
- for (int i = 0; i < timelines.length; i++) {
- mediaSources[i] = new FakeMediaSource(timelines[i], null);
- }
- ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic,
- new FakeShuffleOrder(mediaSources.length), mediaSources);
- MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null);
- try {
- return testRunner.prepareSource();
- } finally {
- testRunner.release();
- }
- }
-
- private static FakeTimeline createFakeTimeline(int periodCount, int windowId) {
- return new FakeTimeline(new TimelineWindowDefinition(periodCount, windowId));
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java
deleted file mode 100644
index 7d3c06b42e..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttDecoderTest.java
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * Copyright (C) 2016 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.text.webvtt;
-
-import android.graphics.Typeface;
-import android.test.InstrumentationTestCase;
-import android.text.Layout.Alignment;
-import android.text.Spanned;
-import android.text.style.BackgroundColorSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
-import android.text.style.TypefaceSpan;
-import android.text.style.UnderlineSpan;
-import com.google.android.exoplayer2.testutil.TestUtil;
-import com.google.android.exoplayer2.text.Cue;
-import com.google.android.exoplayer2.text.SubtitleDecoderException;
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Unit test for {@link WebvttDecoder}.
- */
-public class WebvttDecoderTest extends InstrumentationTestCase {
-
- private static final String TYPICAL_FILE = "webvtt/typical";
- private static final String TYPICAL_WITH_BAD_TIMESTAMPS = "webvtt/typical_with_bad_timestamps";
- private static final String TYPICAL_WITH_IDS_FILE = "webvtt/typical_with_identifiers";
- private static final String TYPICAL_WITH_COMMENTS_FILE = "webvtt/typical_with_comments";
- private static final String WITH_POSITIONING_FILE = "webvtt/with_positioning";
- private static final String WITH_BAD_CUE_HEADER_FILE = "webvtt/with_bad_cue_header";
- private static final String WITH_TAGS_FILE = "webvtt/with_tags";
- private static final String WITH_CSS_STYLES = "webvtt/with_css_styles";
- private static final String WITH_CSS_COMPLEX_SELECTORS = "webvtt/with_css_complex_selectors";
- private static final String EMPTY_FILE = "webvtt/empty";
-
- public void testDecodeEmpty() throws IOException {
- WebvttDecoder decoder = new WebvttDecoder();
- byte[] bytes = TestUtil.getByteArray(getInstrumentation(), EMPTY_FILE);
- try {
- decoder.decode(bytes, bytes.length, false);
- fail();
- } catch (SubtitleDecoderException expected) {
- // Do nothing.
- }
- }
-
- public void testDecodeTypical() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_FILE);
-
- // Test event count.
- assertEquals(4, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
- }
-
- public void testDecodeTypicalWithBadTimestamps() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_BAD_TIMESTAMPS);
-
- // Test event count.
- assertEquals(4, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
- }
-
- public void testDecodeTypicalWithIds() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_IDS_FILE);
-
- // Test event count.
- assertEquals(4, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
- }
-
- public void testDecodeTypicalWithComments() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(TYPICAL_WITH_COMMENTS_FILE);
-
- // test event count
- assertEquals(4, subtitle.getEventTimeCount());
-
- // test cues
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
- }
-
- public void testDecodeWithTags() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_TAGS_FILE);
-
- // Test event count.
- assertEquals(8, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
- assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.");
- assertCue(subtitle, 6, 6000000, 7000000, "This is the &subtitle.");
- }
-
- public void testDecodeWithPositioning() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_POSITIONING_FILE);
- // Test event count.
- assertEquals(12, subtitle.getEventTimeCount());
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.", Alignment.ALIGN_NORMAL,
- Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Cue.ANCHOR_TYPE_START, 0.35f);
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.",
- Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
- Cue.TYPE_UNSET, 0.35f);
- assertCue(subtitle, 4, 4000000, 5000000, "This is the third subtitle.",
- Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
- Cue.TYPE_UNSET, 0.35f);
- assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.",
- Alignment.ALIGN_CENTER, -11f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
- Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
- assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
- Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,
- Cue.ANCHOR_TYPE_END, 0.1f);
- assertCue(subtitle, 10, 10000000, 11000000, "This is the sixth subtitle.",
- Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
- Cue.TYPE_UNSET, 0.35f);
- }
-
- public void testDecodeWithBadCueHeader() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_BAD_CUE_HEADER_FILE);
-
- // Test event count.
- assertEquals(4, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 4000000, 5000000, "This is the third subtitle.");
- }
-
- public void testWebvttWithCssStyle() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_STYLES);
-
- // Test event count.
- assertEquals(8, subtitle.getEventTimeCount());
-
- // Test cues.
- assertCue(subtitle, 0, 0, 1234000, "This is the first subtitle.");
- assertCue(subtitle, 2, 2345000, 3456000, "This is the second subtitle.");
-
- Spanned s1 = getUniqueSpanTextAt(subtitle, 0);
- Spanned s2 = getUniqueSpanTextAt(subtitle, 2345000);
- Spanned s3 = getUniqueSpanTextAt(subtitle, 20000000);
- Spanned s4 = getUniqueSpanTextAt(subtitle, 25000000);
- assertEquals(1, s1.getSpans(0, s1.length(), ForegroundColorSpan.class).length);
- assertEquals(1, s1.getSpans(0, s1.length(), BackgroundColorSpan.class).length);
- assertEquals(2, s2.getSpans(0, s2.length(), ForegroundColorSpan.class).length);
- assertEquals(1, s3.getSpans(10, s3.length(), UnderlineSpan.class).length);
- assertEquals(2, s4.getSpans(0, 16, BackgroundColorSpan.class).length);
- assertEquals(1, s4.getSpans(17, s4.length(), StyleSpan.class).length);
- assertEquals(Typeface.BOLD, s4.getSpans(17, s4.length(), StyleSpan.class)[0].getStyle());
- }
-
- public void testWithComplexCssSelectors() throws IOException, SubtitleDecoderException {
- WebvttSubtitle subtitle = getSubtitleForTestAsset(WITH_CSS_COMPLEX_SELECTORS);
- Spanned text = getUniqueSpanTextAt(subtitle, 0);
- assertEquals(1, text.getSpans(30, text.length(), ForegroundColorSpan.class).length);
- assertEquals(0xFFEE82EE,
- text.getSpans(30, text.length(), ForegroundColorSpan.class)[0].getForegroundColor());
- assertEquals(1, text.getSpans(30, text.length(), TypefaceSpan.class).length);
- assertEquals("courier", text.getSpans(30, text.length(), TypefaceSpan.class)[0].getFamily());
-
- text = getUniqueSpanTextAt(subtitle, 2000000);
- assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
- assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
-
- text = getUniqueSpanTextAt(subtitle, 2500000);
- assertEquals(1, text.getSpans(5, text.length(), StyleSpan.class).length);
- assertEquals(Typeface.BOLD, text.getSpans(5, text.length(), StyleSpan.class)[0].getStyle());
- assertEquals(1, text.getSpans(5, text.length(), TypefaceSpan.class).length);
- assertEquals("courier", text.getSpans(5, text.length(), TypefaceSpan.class)[0].getFamily());
-
- text = getUniqueSpanTextAt(subtitle, 4000000);
- assertEquals(0, text.getSpans(6, 22, StyleSpan.class).length);
- assertEquals(1, text.getSpans(30, text.length(), StyleSpan.class).length);
- assertEquals(Typeface.BOLD, text.getSpans(30, text.length(), StyleSpan.class)[0].getStyle());
-
- text = getUniqueSpanTextAt(subtitle, 5000000);
- assertEquals(0, text.getSpans(9, 17, StyleSpan.class).length);
- assertEquals(1, text.getSpans(19, text.length(), StyleSpan.class).length);
- assertEquals(Typeface.ITALIC, text.getSpans(19, text.length(), StyleSpan.class)[0].getStyle());
- }
-
- private WebvttSubtitle getSubtitleForTestAsset(String asset) throws IOException,
- SubtitleDecoderException {
- WebvttDecoder decoder = new WebvttDecoder();
- byte[] bytes = TestUtil.getByteArray(getInstrumentation(), asset);
- return decoder.decode(bytes, bytes.length, false);
- }
-
- private Spanned getUniqueSpanTextAt(WebvttSubtitle sub, long timeUs) {
- return (Spanned) sub.getCues(timeUs).get(0).text;
- }
-
- private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
- int endTimeUs, String text) {
- assertCue(subtitle, eventTimeIndex, startTimeUs, endTimeUs, text, null, Cue.DIMEN_UNSET,
- Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
- }
-
- private static void assertCue(WebvttSubtitle subtitle, int eventTimeIndex, long startTimeUs,
- int endTimeUs, String text, Alignment textAlignment, float line, int lineType, int lineAnchor,
- float position, int positionAnchor, float size) {
- assertEquals(startTimeUs, subtitle.getEventTime(eventTimeIndex));
- assertEquals(endTimeUs, subtitle.getEventTime(eventTimeIndex + 1));
- List cues = subtitle.getCues(subtitle.getEventTime(eventTimeIndex));
- assertEquals(1, cues.size());
- // Assert cue properties.
- Cue cue = cues.get(0);
- assertEquals(text, cue.text.toString());
- assertEquals(textAlignment, cue.textAlignment);
- assertEquals(line, cue.line);
- assertEquals(lineType, cue.lineType);
- assertEquals(lineAnchor, cue.lineAnchor);
- assertEquals(position, cue.position);
- assertEquals(positionAnchor, cue.positionAnchor);
- assertEquals(size, cue.size);
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
index e19f7ad033..83a978219e 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.upstream;
+import static com.google.common.truth.Truth.assertThat;
+
import android.app.Instrumentation;
import android.content.ContentProvider;
import android.content.ContentResolver;
@@ -75,7 +77,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase {
fail();
} catch (ContentDataSource.ContentDataSourceException e) {
// Expected.
- assertTrue(e.getCause() instanceof FileNotFoundException);
+ assertThat(e.getCause()).isInstanceOf(FileNotFoundException.class);
} finally {
dataSource.close();
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
index 7f6e203c20..9791fdb46f 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java
@@ -1,7 +1,9 @@
package com.google.android.exoplayer2.upstream.cache;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import android.test.InstrumentationTestCase;
-import android.test.MoreAsserts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
@@ -9,10 +11,8 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
-import junit.framework.AssertionFailedError;
/**
* Tests {@link CachedContentIndex}.
@@ -36,6 +36,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
+ super.setUp();
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@@ -43,6 +44,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
@Override
protected void tearDown() throws Exception {
Util.recursiveDelete(cacheDir);
+ super.tearDown();
}
public void testAddGetRemove() throws Exception {
@@ -53,48 +55,46 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Add two CachedContents with add methods
CachedContent cachedContent1 = new CachedContent(5, key1, 10);
index.addNew(cachedContent1);
- CachedContent cachedContent2 = index.add(key2);
- assertTrue(cachedContent1.id != cachedContent2.id);
+ CachedContent cachedContent2 = index.getOrAdd(key2);
+ assertThat(cachedContent1.id != cachedContent2.id).isTrue();
// add a span
File cacheSpanFile = SimpleCacheSpanTest
.createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30);
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
- assertNotNull(span);
+ assertThat(span).isNotNull();
cachedContent1.addSpan(span);
// Check if they are added and get method returns null if the key isn't found
- assertEquals(cachedContent1, index.get(key1));
- assertEquals(cachedContent2, index.get(key2));
- assertNull(index.get(key3));
+ assertThat(index.get(key1)).isEqualTo(cachedContent1);
+ assertThat(index.get(key2)).isEqualTo(cachedContent2);
+ assertThat(index.get(key3)).isNull();
// test getAll()
Collection cachedContents = index.getAll();
- assertEquals(2, cachedContents.size());
- assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents));
+ assertThat(cachedContents).containsExactly(cachedContent1, cachedContent2);
// test getKeys()
Set keys = index.getKeys();
- assertEquals(2, keys.size());
- assertTrue(Arrays.asList(key1, key2).containsAll(keys));
+ assertThat(keys).containsExactly(key1, key2);
// test getKeyForId()
- assertEquals(key1, index.getKeyForId(cachedContent1.id));
- assertEquals(key2, index.getKeyForId(cachedContent2.id));
+ assertThat(index.getKeyForId(cachedContent1.id)).isEqualTo(key1);
+ assertThat(index.getKeyForId(cachedContent2.id)).isEqualTo(key2);
// test remove()
- index.removeEmpty(key2);
- index.removeEmpty(key3);
- assertEquals(cachedContent1, index.get(key1));
- assertNull(index.get(key2));
- assertTrue(cacheSpanFile.exists());
+ index.maybeRemove(key2);
+ index.maybeRemove(key3);
+ assertThat(index.get(key1)).isEqualTo(cachedContent1);
+ assertThat(index.get(key2)).isNull();
+ assertThat(cacheSpanFile.exists()).isTrue();
// test removeEmpty()
index.addNew(cachedContent2);
index.removeEmpty();
- assertEquals(cachedContent1, index.get(key1));
- assertNull(index.get(key2));
- assertTrue(cacheSpanFile.exists());
+ assertThat(index.get(key1)).isEqualTo(cachedContent1);
+ assertThat(index.get(key2)).isNull();
+ assertThat(cacheSpanFile.exists()).isTrue();
}
public void testStoreAndLoad() throws Exception {
@@ -107,11 +107,11 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
fos.close();
index.load();
- assertEquals(2, index.getAll().size());
- assertEquals(5, index.assignIdForKey("ABCDE"));
- assertEquals(10, index.getContentLength("ABCDE"));
- assertEquals(2, index.assignIdForKey("KLMNO"));
- assertEquals(2560, index.getContentLength("KLMNO"));
+ assertThat(index.getAll()).hasSize(2);
+ assertThat(index.assignIdForKey("ABCDE")).isEqualTo(5);
+ assertThat(index.getContentLength("ABCDE")).isEqualTo(10);
+ assertThat(index.assignIdForKey("KLMNO")).isEqualTo(2);
+ assertThat(index.getContentLength("KLMNO")).isEqualTo(2560);
}
public void testStoreV1() throws Exception {
@@ -122,13 +122,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
byte[] buffer = new byte[testIndexV1File.length];
FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
- assertEquals(testIndexV1File.length, fos.read(buffer));
- assertEquals(-1, fos.read());
+ assertThat(fos.read(buffer)).isEqualTo(testIndexV1File.length);
+ assertThat(fos.read()).isEqualTo(-1);
fos.close();
// TODO: The order of the CachedContent stored in index file isn't defined so this test may fail
// on a different implementation of the underlying set
- MoreAsserts.assertEquals(testIndexV1File, buffer);
+ assertThat(buffer).isEqualTo(testIndexV1File);
}
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
@@ -136,29 +136,29 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
final String key2 = "key2";
int id1 = index.assignIdForKey(key1);
int id2 = index.assignIdForKey(key2);
- assertEquals(key1, index.getKeyForId(id1));
- assertEquals(key2, index.getKeyForId(id2));
- assertTrue(id1 != id2);
- assertEquals(id1, index.assignIdForKey(key1));
- assertEquals(id2, index.assignIdForKey(key2));
+ assertThat(index.getKeyForId(id1)).isEqualTo(key1);
+ assertThat(index.getKeyForId(id2)).isEqualTo(key2);
+ assertThat(id1 != id2).isTrue();
+ assertThat(index.assignIdForKey(key1)).isEqualTo(id1);
+ assertThat(index.assignIdForKey(key2)).isEqualTo(id2);
}
public void testSetGetContentLength() throws Exception {
final String key1 = "key1";
- assertEquals(C.LENGTH_UNSET, index.getContentLength(key1));
+ assertThat(index.getContentLength(key1)).isEqualTo(C.LENGTH_UNSET);
index.setContentLength(key1, 10);
- assertEquals(10, index.getContentLength(key1));
+ assertThat(index.getContentLength(key1)).isEqualTo(10);
}
public void testGetNewId() throws Exception {
SparseArray idToKey = new SparseArray<>();
- assertEquals(0, CachedContentIndex.getNewId(idToKey));
+ assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(10, "");
- assertEquals(11, CachedContentIndex.getNewId(idToKey));
+ assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(11);
idToKey.put(Integer.MAX_VALUE, "");
- assertEquals(0, CachedContentIndex.getNewId(idToKey));
+ assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(0);
idToKey.put(0, "");
- assertEquals(1, CachedContentIndex.getNewId(idToKey));
+ assertThat(CachedContentIndex.getNewId(idToKey)).isEqualTo(1);
}
public void testEncryption() throws Exception {
@@ -171,36 +171,40 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
File file2 = new File(cacheDir, "file2compare");
- assertTrue(file1.renameTo(file2));
+ assertThat(file1.renameTo(file2)).isTrue();
// Write a new index file
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
- assertEquals(file2.length(), file1.length());
+ assertThat(file1.length()).isEqualTo(file2.length());
// Assert file content is different
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read(); ) {
- assertTrue(b != -1);
+ assertThat(b != -1).isTrue();
}
boolean threw = false;
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key2));
- } catch (AssertionFailedError e) {
+ } catch (AssertionError e) {
threw = true;
}
- assertTrue("Encrypted index file can not be read with different encryption key", threw);
+ assertWithMessage("Encrypted index file can not be read with different encryption key")
+ .that(threw)
+ .isTrue();
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir));
- } catch (AssertionFailedError e) {
+ } catch (AssertionError e) {
threw = true;
}
- assertTrue("Encrypted index file can not be read without encryption key", threw);
+ assertWithMessage("Encrypted index file can not be read without encryption key")
+ .that(threw)
+ .isTrue();
// Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
@@ -213,19 +217,51 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
}
+ public void testRemoveEmptyNotLockedCachedContent() throws Exception {
+ CachedContent cachedContent = new CachedContent(5, "key1", 10);
+ index.addNew(cachedContent);
+
+ index.maybeRemove(cachedContent.key);
+
+ assertThat(index.get(cachedContent.key)).isNull();
+ }
+
+ public void testCantRemoveNotEmptyCachedContent() throws Exception {
+ CachedContent cachedContent = new CachedContent(5, "key1", 10);
+ index.addNew(cachedContent);
+ File cacheSpanFile =
+ SimpleCacheSpanTest.createCacheSpanFile(cacheDir, cachedContent.id, 10, 20, 30);
+ SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
+ cachedContent.addSpan(span);
+
+ index.maybeRemove(cachedContent.key);
+
+ assertThat(index.get(cachedContent.key)).isNotNull();
+ }
+
+ public void testCantRemoveLockedCachedContent() throws Exception {
+ CachedContent cachedContent = new CachedContent(5, "key1", 10);
+ cachedContent.setLocked(true);
+ index.addNew(cachedContent);
+
+ index.maybeRemove(cachedContent.key);
+
+ assertThat(index.get(cachedContent.key)).isNotNull();
+ }
+
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
index.addNew(new CachedContent(5, "key1", 10));
- index.add("key2");
+ index.getOrAdd("key2");
index.store();
index2.load();
Set keys = index.getKeys();
Set keys2 = index2.getKeys();
- assertEquals(keys, keys2);
+ assertThat(keys2).isEqualTo(keys);
for (String key : keys) {
- assertEquals(index.getContentLength(key), index2.getContentLength(key));
- assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
+ assertThat(index2.getContentLength(key)).isEqualTo(index.getContentLength(key));
+ assertThat(index2.get(key).getSpans()).isEqualTo(index.get(key).getSpans());
}
}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java
deleted file mode 100644
index f40ae0bc7e..0000000000
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CachedRegionTrackerTest.java
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright (C) 2016 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.upstream.cache;
-
-import android.test.InstrumentationTestCase;
-import com.google.android.exoplayer2.extractor.ChunkIndex;
-import com.google.android.exoplayer2.testutil.MockitoUtil;
-import com.google.android.exoplayer2.util.Util;
-import java.io.File;
-import java.io.IOException;
-import org.mockito.Mock;
-
-/**
- * Tests for {@link CachedRegionTracker}.
- */
-public final class CachedRegionTrackerTest extends InstrumentationTestCase {
-
- private static final String CACHE_KEY = "abc";
- private static final long MS_IN_US = 1000;
-
- // 5 chunks, each 20 bytes long and 100 ms long.
- private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
- new int[] {20, 20, 20, 20, 20},
- new long[] {100, 120, 140, 160, 180},
- new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
- new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
-
- @Mock private Cache cache;
- private CachedRegionTracker tracker;
-
- private CachedContentIndex index;
- private File cacheDir;
-
- @Override
- protected void setUp() throws Exception {
- MockitoUtil.setUpMockito(this);
-
- tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
- cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
- index = new CachedContentIndex(cacheDir);
- }
-
- @Override
- protected void tearDown() throws Exception {
- Util.recursiveDelete(cacheDir);
- }
-
- public void testGetRegion_noSpansInCache() {
- assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
- assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
- }
-
- public void testGetRegion_fullyCached() throws Exception {
- tracker.onSpanAdded(
- cache,
- newCacheSpan(100, 100));
-
- assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
- assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
- }
-
- public void testGetRegion_partiallyCached() throws Exception {
- tracker.onSpanAdded(
- cache,
- newCacheSpan(100, 40));
-
- assertEquals(200, tracker.getRegionEndTimeMs(101));
- assertEquals(200, tracker.getRegionEndTimeMs(121));
- }
-
- public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
- tracker.onSpanAdded(
- cache,
- newCacheSpan(100, 20));
- tracker.onSpanAdded(
- cache,
- newCacheSpan(120, 20));
-
- assertEquals(200, tracker.getRegionEndTimeMs(101));
- assertEquals(200, tracker.getRegionEndTimeMs(121));
- }
-
- public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
- // Start with the full stream in cache.
- tracker.onSpanAdded(
- cache,
- newCacheSpan(100, 100));
-
- // Remove the middle bit.
- tracker.onSpanRemoved(
- cache,
- newCacheSpan(140, 40));
-
- assertEquals(200, tracker.getRegionEndTimeMs(101));
- assertEquals(200, tracker.getRegionEndTimeMs(121));
-
- assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
- }
-
- public void testGetRegion_subchunkEstimation() throws Exception {
- tracker.onSpanAdded(
- cache,
- newCacheSpan(100, 10));
-
- assertEquals(50, tracker.getRegionEndTimeMs(101));
- assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
- }
-
- private CacheSpan newCacheSpan(int position, int length) throws IOException {
- return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
- }
-
-}
diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
index 8c684b1cb3..637a19cdd2 100644
--- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
+++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
@@ -15,6 +15,9 @@
*/
package com.google.android.exoplayer2.upstream.cache;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
@@ -48,6 +51,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
+ super.setUp();
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@@ -55,6 +59,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
@Override
protected void tearDown() throws Exception {
Util.recursiveDelete(cacheDir);
+ super.tearDown();
}
public void testCacheFile() throws Exception {
@@ -86,39 +91,39 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
for (File file : cacheDir.listFiles()) {
SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, index);
if (file.equals(wrongEscapedV2file)) {
- assertNull(cacheEntry);
+ assertThat(cacheEntry).isNull();
} else {
- assertNotNull(cacheEntry);
+ assertThat(cacheEntry).isNotNull();
}
}
- assertTrue(v3file.exists());
- assertFalse(v2file.exists());
- assertTrue(wrongEscapedV2file.exists());
- assertFalse(v1File.exists());
+ assertThat(v3file.exists()).isTrue();
+ assertThat(v2file.exists()).isFalse();
+ assertThat(wrongEscapedV2file.exists()).isTrue();
+ assertThat(v1File.exists()).isFalse();
File[] files = cacheDir.listFiles();
- assertEquals(4, files.length);
+ assertThat(files).hasLength(4);
Set keys = index.getKeys();
- assertEquals("There should be only one key for all files.", 1, keys.size());
- assertTrue(keys.contains(key));
+ assertWithMessage("There should be only one key for all files.").that(keys).hasSize(1);
+ assertThat(keys).contains(key);
TreeSet spans = index.get(key).getSpans();
- assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty());
+ assertWithMessage("upgradeOldFiles() shouldn't add any spans.").that(spans.isEmpty()).isTrue();
HashMap cachedPositions = new HashMap<>();
for (File file : files) {
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, index);
if (cacheSpan != null) {
- assertEquals(key, cacheSpan.key);
+ assertThat(cacheSpan.key).isEqualTo(key);
cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp);
}
}
- assertEquals(1, (long) cachedPositions.get((long) 0));
- assertEquals(2, (long) cachedPositions.get((long) 1));
- assertEquals(6, (long) cachedPositions.get((long) 5));
+ assertThat(cachedPositions.get((long) 0)).isEqualTo(1);
+ assertThat(cachedPositions.get((long) 1)).isEqualTo(2);
+ assertThat(cachedPositions.get((long) 5)).isEqualTo(6);
}
private static void createTestFile(File file, int length) throws IOException {
@@ -141,14 +146,14 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
File cacheFile = createCacheSpanFile(cacheDir, id, offset, 1, lastAccessTimestamp);
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
String message = cacheFile.toString();
- assertNotNull(message, cacheSpan);
- assertEquals(message, cacheDir, cacheFile.getParentFile());
- assertEquals(message, key, cacheSpan.key);
- assertEquals(message, offset, cacheSpan.position);
- assertEquals(message, 1, cacheSpan.length);
- assertTrue(message, cacheSpan.isCached);
- assertEquals(message, cacheFile, cacheSpan.file);
- assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp);
+ assertWithMessage(message).that(cacheSpan).isNotNull();
+ assertWithMessage(message).that(cacheFile.getParentFile()).isEqualTo(cacheDir);
+ assertWithMessage(message).that(cacheSpan.key).isEqualTo(key);
+ assertWithMessage(message).that(cacheSpan.position).isEqualTo(offset);
+ assertWithMessage(message).that(cacheSpan.length).isEqualTo(1);
+ assertWithMessage(message).that(cacheSpan.isCached).isTrue();
+ assertWithMessage(message).that(cacheSpan.file).isEqualTo(cacheFile);
+ assertWithMessage(message).that(cacheSpan.lastAccessTimestamp).isEqualTo(lastAccessTimestamp);
}
private void assertNullCacheSpan(File parent, String key, long offset,
@@ -156,7 +161,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset,
lastAccessTimestamp);
CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
- assertNull(cacheFile.toString(), cacheSpan);
+ assertWithMessage(cacheFile.toString()).that(cacheSpan).isNull();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
index a4103787d1..8ee9a13c55 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java
@@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
return ADAPTIVE_NOT_SUPPORTED;
}
- // ExoPlayerComponent implementation.
+ // PlayerMessage.Target implementation.
@Override
public void handleMessage(int what, Object object) throws ExoPlaybackException {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java
index 6a35c0c5e8..045f3bfc6e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/C.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java
@@ -23,6 +23,7 @@ import android.media.MediaCodec;
import android.media.MediaFormat;
import android.support.annotation.IntDef;
import android.view.Surface;
+import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -122,13 +123,22 @@ public final class C {
*/
public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
- /**
- * Represents an audio encoding, or an invalid or unset value.
- */
+ /** Represents an audio encoding, or an invalid or unset value. */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
- ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3,
- ENCODING_DTS, ENCODING_DTS_HD})
+ @IntDef({
+ Format.NO_VALUE,
+ ENCODING_INVALID,
+ ENCODING_PCM_8BIT,
+ ENCODING_PCM_16BIT,
+ ENCODING_PCM_24BIT,
+ ENCODING_PCM_32BIT,
+ ENCODING_PCM_FLOAT,
+ ENCODING_AC3,
+ ENCODING_E_AC3,
+ ENCODING_DTS,
+ ENCODING_DTS_HD,
+ ENCODING_DOLBY_TRUEHD
+ })
public @interface Encoding {}
/**
@@ -138,46 +148,28 @@ public final class C {
@IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT,
ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT})
public @interface PcmEncoding {}
- /**
- * @see AudioFormat#ENCODING_INVALID
- */
+ /** @see AudioFormat#ENCODING_INVALID */
public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
- /**
- * @see AudioFormat#ENCODING_PCM_8BIT
- */
+ /** @see AudioFormat#ENCODING_PCM_8BIT */
public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
- /**
- * @see AudioFormat#ENCODING_PCM_16BIT
- */
+ /** @see AudioFormat#ENCODING_PCM_16BIT */
public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
- /**
- * PCM encoding with 24 bits per sample.
- */
+ /** PCM encoding with 24 bits per sample. */
public static final int ENCODING_PCM_24BIT = 0x80000000;
- /**
- * PCM encoding with 32 bits per sample.
- */
+ /** PCM encoding with 32 bits per sample. */
public static final int ENCODING_PCM_32BIT = 0x40000000;
- /**
- * @see AudioFormat#ENCODING_PCM_FLOAT
- */
+ /** @see AudioFormat#ENCODING_PCM_FLOAT */
public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
- /**
- * @see AudioFormat#ENCODING_AC3
- */
+ /** @see AudioFormat#ENCODING_AC3 */
public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
- /**
- * @see AudioFormat#ENCODING_E_AC3
- */
+ /** @see AudioFormat#ENCODING_E_AC3 */
public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
- /**
- * @see AudioFormat#ENCODING_DTS
- */
+ /** @see AudioFormat#ENCODING_DTS */
public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
- /**
- * @see AudioFormat#ENCODING_DTS_HD
- */
+ /** @see AudioFormat#ENCODING_DTS_HD */
public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
+ /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */
+ public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
/**
* @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND
@@ -651,37 +643,37 @@ public final class C {
public static final UUID PLAYREADY_UUID = new UUID(0x9A04F07998404286L, 0xAB92E65BE0885F95L);
/**
- * The type of a message that can be passed to a video {@link Renderer} via
- * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be the target {@link Surface}, or null.
+ * The type of a message that can be passed to a video {@link Renderer} via {@link
+ * ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or
+ * null.
*/
public static final int MSG_SET_SURFACE = 1;
/**
- * A type of a message that can be passed to an audio {@link Renderer} via
- * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be a {@link Float} with 0 being silence and 1 being unity gain.
+ * A type of a message that can be passed to an audio {@link Renderer} via {@link
+ * ExoPlayer#createMessage(Target)}. The message payload should be a {@link Float} with 0 being
+ * silence and 1 being unity gain.
*/
public static final int MSG_SET_VOLUME = 2;
/**
- * A type of a message that can be passed to an audio {@link Renderer} via
- * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
- * should be an {@link com.google.android.exoplayer2.audio.AudioAttributes} instance that will
- * configure the underlying audio track. If not set, the default audio attributes will be used.
- * They are suitable for general media playback.
- *
- * Setting the audio attributes during playback may introduce a short gap in audio output as the
- * audio track is recreated. A new audio session id will also be generated.
- *
- * If tunneling is enabled by the track selector, the specified audio attributes will be ignored,
- * but they will take effect if audio is later played without tunneling.
- *
- * If the device is running a build before platform API version 21, audio attributes cannot be set
- * directly on the underlying audio track. In this case, the usage will be mapped onto an
+ * A type of a message that can be passed to an audio {@link Renderer} via {@link
+ * ExoPlayer#createMessage(Target)}. The message payload should be an {@link
+ * com.google.android.exoplayer2.audio.AudioAttributes} instance that will configure the
+ * underlying audio track. If not set, the default audio attributes will be used. They are
+ * suitable for general media playback.
+ *
+ *
Setting the audio attributes during playback may introduce a short gap in audio output as
+ * the audio track is recreated. A new audio session id will also be generated.
+ *
+ *
If tunneling is enabled by the track selector, the specified audio attributes will be
+ * ignored, but they will take effect if audio is later played without tunneling.
+ *
+ *
If the device is running a build before platform API version 21, audio attributes cannot be
+ * set directly on the underlying audio track. In this case, the usage will be mapped onto an
* equivalent stream type using {@link Util#getStreamTypeForAudioUsage(int)}.
- *
- * To get audio attributes that are equivalent to a legacy stream type, pass the stream type to
+ *
+ *
To get audio attributes that are equivalent to a legacy stream type, pass the stream type to
* {@link Util#getAudioUsageForStreamType(int)} and use the returned {@link C.AudioUsage} to build
* an audio attributes instance.
*/
@@ -689,17 +681,17 @@ public final class C {
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
- * via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
- * object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
- *
- * Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
+ * via {@link ExoPlayer#createMessage(Target)}. The message payload should be one of the integer
+ * scaling modes in {@link C.VideoScalingMode}.
+ *
+ *
Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
* owned by a {@link android.view.SurfaceView}.
*/
public static final int MSG_SET_SCALING_MODE = 4;
/**
- * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
- * this value.
+ * Applications or extensions may define custom {@code MSG_*} constants that can be passed to
+ * {@link Renderer}s. These custom constants must be greater than or equal to this value.
*/
public static final int MSG_CUSTOM_BASE = 10000;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
index b7b68de7d2..e8ea2f1621 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java
@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.util.Util;
/**
* The default {@link LoadControl} implementation.
*/
-public final class DefaultLoadControl implements LoadControl {
+public class DefaultLoadControl implements LoadControl {
/**
* The default minimum duration of media that the player will attempt to ensure is buffered at all
@@ -90,8 +90,8 @@ public final class DefaultLoadControl implements LoadControl {
allocator,
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
- DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+ DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
DEFAULT_TARGET_BUFFER_BYTES,
DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
}
@@ -166,9 +166,9 @@ public final class DefaultLoadControl implements LoadControl {
this.allocator = allocator;
minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L;
- targetBufferBytesOverwrite = targetBufferBytes;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
+ targetBufferBytesOverwrite = targetBufferBytes;
this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds;
this.priorityTaskManager = priorityTaskManager;
}
@@ -204,16 +204,17 @@ public final class DefaultLoadControl implements LoadControl {
}
@Override
- public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) {
- long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
- return minBufferDurationUs <= 0
- || bufferedDurationUs >= minBufferDurationUs
- || (!prioritizeTimeOverSizeThresholds
- && allocator.getTotalBytesAllocated() >= targetBufferSize);
+ public long getBackBufferDurationUs() {
+ return 0;
}
@Override
- public boolean shouldContinueLoading(long bufferedDurationUs) {
+ public boolean retainBackBufferFromKeyframe() {
+ return false;
+ }
+
+ @Override
+ public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
if (prioritizeTimeOverSizeThresholds) {
@@ -238,6 +239,17 @@ public final class DefaultLoadControl implements LoadControl {
return isBuffering;
}
+ @Override
+ public boolean shouldStartPlayback(
+ long bufferedDurationUs, float playbackSpeed, boolean rebuffering) {
+ bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed);
+ long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs;
+ return minBufferDurationUs <= 0
+ || bufferedDurationUs >= minBufferDurationUs
+ || (!prioritizeTimeOverSizeThresholds
+ && allocator.getTotalBytesAllocated() >= targetBufferSize);
+ }
+
/**
* Calculate target buffer size in bytes based on the selected tracks. The player will try not to
* exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
new file mode 100644
index 0000000000..ed57cec70c
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java
@@ -0,0 +1,178 @@
+/*
+ * 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;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.MediaClock;
+import com.google.android.exoplayer2.util.StandaloneMediaClock;
+
+/**
+ * Default {@link MediaClock} which uses a renderer media clock and falls back to a
+ * {@link StandaloneMediaClock} if necessary.
+ */
+/* package */ final class DefaultMediaClock implements MediaClock {
+
+ /**
+ * Listener interface to be notified of changes to the active playback parameters.
+ */
+ public interface PlaybackParameterListener {
+
+ /**
+ * Called when the active playback parameters changed.
+ *
+ * @param newPlaybackParameters The newly active {@link PlaybackParameters}.
+ */
+ void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters);
+
+ }
+
+ private final StandaloneMediaClock standaloneMediaClock;
+ private final PlaybackParameterListener listener;
+
+ private @Nullable Renderer rendererClockSource;
+ private @Nullable MediaClock rendererClock;
+
+ /**
+ * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use
+ * for the standalone clock implementation.
+ *
+ * @param listener A {@link PlaybackParameterListener} to listen for playback parameter
+ * changes.
+ * @param clock A {@link Clock}.
+ */
+ public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) {
+ this.listener = listener;
+ this.standaloneMediaClock = new StandaloneMediaClock(clock);
+ }
+
+ /**
+ * Starts the standalone fallback clock.
+ */
+ public void start() {
+ standaloneMediaClock.start();
+ }
+
+ /**
+ * Stops the standalone fallback clock.
+ */
+ public void stop() {
+ standaloneMediaClock.stop();
+ }
+
+ /**
+ * Resets the position of the standalone fallback clock.
+ *
+ * @param positionUs The position to set in microseconds.
+ */
+ public void resetPosition(long positionUs) {
+ standaloneMediaClock.resetPosition(positionUs);
+ }
+
+ /**
+ * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the
+ * provided renderer if available.
+ *
+ * @param renderer The renderer which has been enabled.
+ * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media
+ * clock is already provided.
+ */
+ public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException {
+ MediaClock rendererMediaClock = renderer.getMediaClock();
+ if (rendererMediaClock != null && rendererMediaClock != rendererClock) {
+ if (rendererClock != null) {
+ throw ExoPlaybackException.createForUnexpected(
+ new IllegalStateException("Multiple renderer media clocks enabled."));
+ }
+ this.rendererClock = rendererMediaClock;
+ this.rendererClockSource = renderer;
+ rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters());
+ ensureSynced();
+ }
+ }
+
+ /**
+ * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this
+ * renderer if used.
+ *
+ * @param renderer The renderer which has been disabled.
+ */
+ public void onRendererDisabled(Renderer renderer) {
+ if (renderer == rendererClockSource) {
+ this.rendererClock = null;
+ this.rendererClockSource = null;
+ }
+ }
+
+ /**
+ * Syncs internal clock if needed and returns current clock position in microseconds.
+ */
+ public long syncAndGetPositionUs() {
+ if (isUsingRendererClock()) {
+ ensureSynced();
+ return rendererClock.getPositionUs();
+ } else {
+ return standaloneMediaClock.getPositionUs();
+ }
+ }
+
+ // MediaClock implementation.
+
+ @Override
+ public long getPositionUs() {
+ if (isUsingRendererClock()) {
+ return rendererClock.getPositionUs();
+ } else {
+ return standaloneMediaClock.getPositionUs();
+ }
+ }
+
+ @Override
+ public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
+ if (rendererClock != null) {
+ playbackParameters = rendererClock.setPlaybackParameters(playbackParameters);
+ }
+ standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ listener.onPlaybackParametersChanged(playbackParameters);
+ return playbackParameters;
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return rendererClock != null ? rendererClock.getPlaybackParameters()
+ : standaloneMediaClock.getPlaybackParameters();
+ }
+
+ private void ensureSynced() {
+ long rendererClockPositionUs = rendererClock.getPositionUs();
+ standaloneMediaClock.resetPosition(rendererClockPositionUs);
+ PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters();
+ if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) {
+ standaloneMediaClock.setPlaybackParameters(playbackParameters);
+ listener.onPlaybackParametersChanged(playbackParameters);
+ }
+ }
+
+ private boolean isUsingRendererClock() {
+ // Use the renderer clock if the providing renderer has not ended or needs the next sample
+ // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting
+ // stuck if tracks in the current period have uneven durations.
+ // See: https://github.com/google/ExoPlayer/issues/1874.
+ return rendererClockSource != null && !rendererClockSource.isEnded()
+ && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd());
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 2272306117..16074108b1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -185,18 +185,32 @@ public class DefaultRenderersFactory implements RenderersFactory {
}
try {
- Class> clazz =
- Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
- Constructor> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
- VideoRendererEventListener.class, int.class);
- Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
- eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
+ // Full class names used for constructor args so the LINT rule triggers if any of them move.
+ // LINT.IfChange
+ Class> clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
+ Constructor> constructor =
+ clazz.getConstructor(
+ boolean.class,
+ long.class,
+ android.os.Handler.class,
+ com.google.android.exoplayer2.video.VideoRendererEventListener.class,
+ int.class);
+ // LINT.ThenChange(../../../../../../../proguard-rules.txt)
+ Renderer renderer =
+ (Renderer)
+ constructor.newInstance(
+ true,
+ allowedVideoJoiningTimeMs,
+ eventHandler,
+ eventListener,
+ MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibvpxVideoRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
- throw new RuntimeException(e);
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating VP9 extension", e);
}
}
@@ -230,48 +244,67 @@ public class DefaultRenderersFactory implements RenderersFactory {
}
try {
- Class> clazz =
- Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
- Constructor> constructor = clazz.getConstructor(Handler.class,
- AudioRendererEventListener.class, AudioProcessor[].class);
- Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
- audioProcessors);
+ // Full class names used for constructor args so the LINT rule triggers if any of them move.
+ // LINT.IfChange
+ Class> clazz = Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
+ Constructor> constructor =
+ clazz.getConstructor(
+ android.os.Handler.class,
+ com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
+ com.google.android.exoplayer2.audio.AudioProcessor[].class);
+ // LINT.ThenChange(../../../../../../../proguard-rules.txt)
+ Renderer renderer =
+ (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibopusAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
- throw new RuntimeException(e);
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating Opus extension", e);
}
try {
- Class> clazz =
- Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
- Constructor> constructor = clazz.getConstructor(Handler.class,
- AudioRendererEventListener.class, AudioProcessor[].class);
- Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
- audioProcessors);
+ // Full class names used for constructor args so the LINT rule triggers if any of them move.
+ // LINT.IfChange
+ Class> clazz = Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
+ Constructor> constructor =
+ clazz.getConstructor(
+ android.os.Handler.class,
+ com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
+ com.google.android.exoplayer2.audio.AudioProcessor[].class);
+ // LINT.ThenChange(../../../../../../../proguard-rules.txt)
+ Renderer renderer =
+ (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibflacAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
- throw new RuntimeException(e);
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating FLAC extension", e);
}
try {
+ // Full class names used for constructor args so the LINT rule triggers if any of them move.
+ // LINT.IfChange
Class> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
- Constructor> constructor = clazz.getConstructor(Handler.class,
- AudioRendererEventListener.class, AudioProcessor[].class);
- Renderer renderer = (Renderer) constructor.newInstance(eventHandler, eventListener,
- audioProcessors);
+ Constructor> constructor =
+ clazz.getConstructor(
+ android.os.Handler.class,
+ com.google.android.exoplayer2.audio.AudioRendererEventListener.class,
+ com.google.android.exoplayer2.audio.AudioProcessor[].class);
+ // LINT.ThenChange(../../../../../../../proguard-rules.txt)
+ Renderer renderer =
+ (Renderer) constructor.newInstance(eventHandler, eventListener, audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
- throw new RuntimeException(e);
+ // The extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating FFmpeg extension", e);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 915a083657..c13fd6cacd 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import android.os.Looper;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.source.ClippingMediaSource;
@@ -33,40 +34,43 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
/**
- * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from
- * {@link ExoPlayerFactory}.
+ * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link
+ * ExoPlayerFactory}.
*
*
Player components
+ *
* ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the
* type of the media being played, how and where it is stored, and how it is rendered. Rather than
* implementing the loading and rendering of media directly, ExoPlayer implementations delegate this
* work to components that are injected when a player is created or when it's prepared for playback.
* Components common to all ExoPlayer implementations are:
+ *
*
* - A {@link MediaSource} that defines the media to be played, loads the media, and from
- * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)}
- * at the start of playback. The library modules provide default implementations for regular media
- * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource)
- * and HLS (HlsMediaSource), an implementation for loading single media samples
- * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and
- * implementations for building more complex MediaSources from simpler ones
- * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource},
- * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and
- * {@link ClippingMediaSource}).
+ * which the loaded media can be read. A MediaSource is injected via {@link
+ * #prepare(MediaSource)} at the start of playback. The library modules provide default
+ * implementations for regular media files ({@link ExtractorMediaSource}), DASH
+ * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an
+ * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's
+ * most often used for side-loaded subtitle files, and implementations for building more
+ * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link
+ * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link
+ * LoopingMediaSource} and {@link ClippingMediaSource}).
* - {@link Renderer}s that render individual components of the media. The library
- * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
- * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer
- * consumes media from the MediaSource being played. Renderers are injected when the player is
- * created.
+ * provides default implementations for common media types ({@link MediaCodecVideoRenderer},
+ * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A
+ * Renderer consumes media from the MediaSource being played. Renderers are injected when the
+ * player is created.
* - A {@link TrackSelector} that selects tracks provided by the MediaSource to be
- * consumed by each of the available Renderers. The library provides a default implementation
- * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when
- * the player is created.
+ * consumed by each of the available Renderers. The library provides a default implementation
+ * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected
+ * when the player is created.
* - A {@link LoadControl} that controls when the MediaSource buffers more media, and how
- * much media is buffered. The library provides a default implementation
- * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the
- * player is created.
+ * much media is buffered. The library provides a default implementation ({@link
+ * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player
+ * is created.
*
+ *
* An ExoPlayer can be built using the default components provided by the library, but may also
* be built using custom implementations if non-standard behaviors are required. For example a
* custom LoadControl could be injected to change the player's buffering strategy, or a custom
@@ -80,30 +84,31 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
* it's possible to load data from a non-standard source, or through a different network stack.
*
*
Threading model
- * The figure below shows ExoPlayer's threading model.
- *
- *
- *
+ *
+ * The figure below shows ExoPlayer's threading model.
+ *
+ *
*
*
- * - It is recommended that ExoPlayer instances are created and accessed from a single application
- * thread. The application's main thread is ideal. Accessing an instance from multiple threads is
- * discouraged, however if an application does wish to do this then it may do so provided that it
- * ensures accesses are synchronized.
- * - Registered listeners are called on the thread that created the ExoPlayer instance, unless
- * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case,
- * registered listeners will be called on the application's main thread.
- * - An internal playback thread is responsible for playback. Injected player components such as
- * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
- * thread.
- * - When the application performs an operation on the player, for example a seek, a message is
- * delivered to the internal playback thread via a message queue. The internal playback thread
- * consumes messages from the queue and performs the corresponding operations. Similarly, when a
- * playback event occurs on the internal playback thread, a message is delivered to the application
- * thread via a second message queue. The application thread consumes messages from the queue,
- * updating the application visible state and calling corresponding listener methods.
- * - Injected player components may use additional background threads. For example a MediaSource
- * may use background threads to load data. These are implementation specific.
+ * - It is strongly recommended that ExoPlayer instances are created and accessed from a single
+ * application thread. The application's main thread is ideal. Accessing an instance from
+ * multiple threads is discouraged as it may cause synchronization problems.
+ *
- Registered listeners are called on the thread that created the ExoPlayer instance, unless
+ * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that
+ * case, registered listeners will be called on the application's main thread.
+ *
- An internal playback thread is responsible for playback. Injected player components such as
+ * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this
+ * thread.
+ *
- When the application performs an operation on the player, for example a seek, a message is
+ * delivered to the internal playback thread via a message queue. The internal playback thread
+ * consumes messages from the queue and performs the corresponding operations. Similarly, when
+ * a playback event occurs on the internal playback thread, a message is delivered to the
+ * application thread via a second message queue. The application thread consumes messages
+ * from the queue, updating the application visible state and calling corresponding listener
+ * methods.
+ *
- Injected player components may use additional background threads. For example a MediaSource
+ * may use background threads to load data. These are implementation specific.
*
*/
public interface ExoPlayer extends Player {
@@ -114,54 +119,28 @@ public interface ExoPlayer extends Player {
@Deprecated
interface EventListener extends Player.EventListener {}
- /**
- * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
- *
- * Messages can be delivered to a component via {@link #sendMessages} and
- * {@link #blockingSendMessages}.
- */
- interface ExoPlayerComponent {
+ /** @deprecated Use {@link PlayerMessage.Target} instead. */
+ @Deprecated
+ interface ExoPlayerComponent extends PlayerMessage.Target {}
- /**
- * Handles a message delivered to the component. Called on the playback thread.
- *
- * @param messageType The message type.
- * @param message The message.
- * @throws ExoPlaybackException If an error occurred whilst handling the message.
- */
- void handleMessage(int messageType, Object message) throws ExoPlaybackException;
-
- }
-
- /**
- * Defines a message and a target {@link ExoPlayerComponent} to receive it.
- */
+ /** @deprecated Use {@link PlayerMessage} instead. */
+ @Deprecated
final class ExoPlayerMessage {
- /**
- * The target to receive the message.
- */
- public final ExoPlayerComponent target;
- /**
- * The type of the message.
- */
+ /** The target to receive the message. */
+ public final PlayerMessage.Target target;
+ /** The type of the message. */
public final int messageType;
- /**
- * The message.
- */
+ /** The message. */
public final Object message;
- /**
- * @param target The target of the message.
- * @param messageType The message type.
- * @param message The message.
- */
- public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) {
+ /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */
+ @Deprecated
+ public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) {
this.target = target;
this.messageType = messageType;
this.message = message;
}
-
}
/**
@@ -235,20 +214,31 @@ public interface ExoPlayer extends Player {
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
/**
- * Sends messages to their target components. The messages are delivered on the playback thread.
- * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player
- * as an error.
- *
- * @param messages The messages to be sent.
+ * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message
+ * will be delivered immediately without blocking on the playback thread. The default {@link
+ * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a
+ * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be
+ * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}.
+ * Alternatively, the message can be sent at a specific window using {@link
+ * PlayerMessage#setPosition(int, long)}.
*/
+ PlayerMessage createMessage(PlayerMessage.Target target);
+
+ /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */
+ @Deprecated
void sendMessages(ExoPlayerMessage... messages);
/**
- * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have
- * been delivered.
- *
- * @param messages The messages to be sent.
+ * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link
+ * PlayerMessage#blockUntilDelivered()}.
*/
+ @Deprecated
void blockingSendMessages(ExoPlayerMessage... messages);
+ /**
+ * Sets the parameters that control how seek operations are performed.
+ *
+ * @param seekParameters The seek parameters, or {@code null} to use the defaults.
+ */
+ void setSeekParameters(@Nullable SeekParameters seekParameters);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
index b647e541bc..821671e34e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java
@@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.util.Clock;
/**
* A factory for {@link ExoPlayer} instances.
@@ -160,7 +161,7 @@ public final class ExoPlayerFactory {
*/
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
index 349751eb59..83bbdd1157 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java
@@ -21,6 +21,8 @@ import android.os.Looper;
import android.os.Message;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.TrackGroupArray;
@@ -29,7 +31,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
/**
@@ -41,23 +46,20 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final Renderer[] renderers;
private final TrackSelector trackSelector;
- private final TrackSelectionArray emptyTrackSelections;
+ private final TrackSelectorResult emptyTrackSelectorResult;
private final Handler eventHandler;
private final ExoPlayerImplInternal internalPlayer;
+ private final Handler internalPlayerHandler;
private final CopyOnWriteArraySet listeners;
private final Timeline.Window window;
private final Timeline.Period period;
- private boolean tracksSelected;
private boolean playWhenReady;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
- private int playbackState;
- private int pendingSeekAcks;
- private int pendingPrepareAcks;
- private boolean isLoading;
- private TrackGroupArray trackGroups;
- private TrackSelectionArray trackSelections;
+ private int pendingOperationAcks;
+ private boolean hasPendingPrepare;
+ private boolean hasPendingSeek;
private PlaybackParameters playbackParameters;
// Playback information when there is no pending seek/set source operation.
@@ -74,9 +76,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @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.
+ * @param clock The {@link Clock} that will be used by the instance.
*/
@SuppressLint("HandlerLeak")
- public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
+ public ExoPlayerImpl(
+ Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " ["
+ ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
@@ -85,13 +89,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.playWhenReady = false;
this.repeatMode = Player.REPEAT_MODE_OFF;
this.shuffleModeEnabled = false;
- this.playbackState = Player.STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
- emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
+ emptyTrackSelectorResult =
+ new TrackSelectorResult(
+ TrackGroupArray.EMPTY,
+ new boolean[renderers.length],
+ new TrackSelectionArray(new TrackSelection[renderers.length]),
+ null,
+ new RendererConfiguration[renderers.length]);
window = new Timeline.Window();
period = new Timeline.Period();
- trackGroups = TrackGroupArray.EMPTY;
- trackSelections = emptyTrackSelections;
playbackParameters = PlaybackParameters.DEFAULT;
Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper();
eventHandler = new Handler(eventLooper) {
@@ -100,9 +107,31 @@ import java.util.concurrent.CopyOnWriteArraySet;
ExoPlayerImpl.this.handleEvent(msg);
}
};
- playbackInfo = new PlaybackInfo(Timeline.EMPTY, null, 0, 0);
- internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
- repeatMode, shuffleModeEnabled, eventHandler, this);
+ playbackInfo =
+ new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult);
+ internalPlayer =
+ new ExoPlayerImplInternal(
+ renderers,
+ trackSelector,
+ emptyTrackSelectorResult,
+ loadControl,
+ playWhenReady,
+ repeatMode,
+ shuffleModeEnabled,
+ eventHandler,
+ this,
+ clock);
+ internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper());
+ }
+
+ @Override
+ public VideoComponent getVideoComponent() {
+ return null;
+ }
+
+ @Override
+ public TextComponent getTextComponent() {
+ return null;
}
@Override
@@ -122,7 +151,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public int getPlaybackState() {
- return playbackState;
+ return playbackInfo.playbackState;
}
@Override
@@ -132,34 +161,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
- if (!resetPosition) {
- maskingWindowIndex = getCurrentWindowIndex();
- maskingPeriodIndex = getCurrentPeriodIndex();
- maskingWindowPositionMs = getCurrentPosition();
- } else {
- maskingWindowIndex = 0;
- maskingPeriodIndex = 0;
- maskingWindowPositionMs = 0;
- }
- if (resetState) {
- if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) {
- playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null);
- for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest);
- }
- }
- if (tracksSelected) {
- tracksSelected = false;
- trackGroups = TrackGroupArray.EMPTY;
- trackSelections = emptyTrackSelections;
- trackSelector.onSelectionActivated(null);
- for (Player.EventListener listener : listeners) {
- listener.onTracksChanged(trackGroups, trackSelections);
- }
- }
- }
- pendingPrepareAcks++;
+ PlaybackInfo playbackInfo =
+ getResetPlaybackInfo(
+ resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING);
+ // Trigger internal prepare first before updating the playback info and notifying external
+ // listeners to ensure that new operations issued in the listener notifications reach the
+ // player after this prepare. The internal player can't change the playback info immediately
+ // because it uses a callback.
+ hasPendingPrepare = true;
+ pendingOperationAcks++;
internalPlayer.prepare(mediaSource, resetPosition);
+ updatePlaybackInfo(
+ playbackInfo,
+ /* positionDiscontinuity= */ false,
+ /* ignored */ DISCONTINUITY_REASON_INTERNAL,
+ TIMELINE_CHANGE_REASON_RESET,
+ /* seekProcessed= */ false);
}
@Override
@@ -168,7 +185,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
this.playWhenReady = playWhenReady;
internalPlayer.setPlayWhenReady(playWhenReady);
for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackState);
+ listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
}
}
}
@@ -212,7 +229,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public boolean isLoading() {
- return isLoading;
+ return playbackInfo.isLoading;
}
@Override
@@ -236,37 +253,33 @@ import java.util.concurrent.CopyOnWriteArraySet;
if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
}
+ hasPendingSeek = true;
+ pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
- if (pendingSeekAcks == 0) {
- for (Player.EventListener listener : listeners) {
- listener.onSeekProcessed();
- }
- }
+ eventHandler
+ .obtainMessage(
+ ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED,
+ /* operationAcks */ 1,
+ /* positionDiscontinuityReason */ C.INDEX_UNSET,
+ playbackInfo)
+ .sendToTarget();
return;
}
- pendingSeekAcks++;
maskingWindowIndex = windowIndex;
if (timeline.isEmpty()) {
maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs;
maskingPeriodIndex = 0;
} else {
- timeline.getWindow(windowIndex, window);
- long windowPositionUs = positionMs == C.TIME_UNSET ? window.getDefaultPositionUs()
- : C.msToUs(positionMs);
- int periodIndex = window.firstPeriodIndex;
- long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs;
- long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs();
- while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs
- && periodIndex < window.lastPeriodIndex) {
- periodPositionUs -= periodDurationUs;
- periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs();
- }
+ long windowPositionUs = positionMs == C.TIME_UNSET
+ ? timeline.getWindow(windowIndex, window).getDefaultPositionUs() : C.msToUs(positionMs);
+ Pair periodIndexAndPositon =
+ timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
maskingWindowPositionMs = C.usToMs(windowPositionUs);
- maskingPeriodIndex = periodIndex;
+ maskingPeriodIndex = periodIndexAndPositon.first;
}
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
for (Player.EventListener listener : listeners) {
@@ -287,9 +300,38 @@ import java.util.concurrent.CopyOnWriteArraySet;
return playbackParameters;
}
+ @Override
+ public void setSeekParameters(@Nullable SeekParameters seekParameters) {
+ if (seekParameters == null) {
+ seekParameters = SeekParameters.DEFAULT;
+ }
+ internalPlayer.setSeekParameters(seekParameters);
+ }
+
@Override
public void stop() {
- internalPlayer.stop();
+ stop(/* reset= */ false);
+ }
+
+ @Override
+ public void stop(boolean reset) {
+ PlaybackInfo playbackInfo =
+ getResetPlaybackInfo(
+ /* resetPosition= */ reset,
+ /* resetState= */ reset,
+ /* playbackState= */ Player.STATE_IDLE);
+ // Trigger internal stop first before updating the playback info and notifying external
+ // listeners to ensure that new operations issued in the listener notifications reach the
+ // player after this stop. The internal player can't change the playback info immediately
+ // because it uses a callback.
+ pendingOperationAcks++;
+ internalPlayer.stop(reset);
+ updatePlaybackInfo(
+ playbackInfo,
+ /* positionDiscontinuity= */ false,
+ /* ignored */ DISCONTINUITY_REASON_INTERNAL,
+ TIMELINE_CHANGE_REASON_RESET,
+ /* seekProcessed= */ false);
}
@Override
@@ -303,12 +345,47 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void sendMessages(ExoPlayerMessage... messages) {
- internalPlayer.sendMessages(messages);
+ for (ExoPlayerMessage message : messages) {
+ createMessage(message.target).setType(message.messageType).setPayload(message.message).send();
+ }
+ }
+
+ @Override
+ public PlayerMessage createMessage(Target target) {
+ return new PlayerMessage(
+ internalPlayer,
+ target,
+ playbackInfo.timeline,
+ getCurrentWindowIndex(),
+ internalPlayerHandler);
}
@Override
public void blockingSendMessages(ExoPlayerMessage... messages) {
- internalPlayer.blockingSendMessages(messages);
+ List playerMessages = new ArrayList<>();
+ for (ExoPlayerMessage message : messages) {
+ playerMessages.add(
+ createMessage(message.target)
+ .setType(message.messageType)
+ .setPayload(message.message)
+ .send());
+ }
+ boolean wasInterrupted = false;
+ for (PlayerMessage message : playerMessages) {
+ boolean blockMessage = true;
+ while (blockMessage) {
+ try {
+ message.blockUntilDelivered();
+ blockMessage = false;
+ } catch (InterruptedException e) {
+ wasInterrupted = true;
+ }
+ }
+ }
+ if (wasInterrupted) {
+ // Restore the interrupted status.
+ Thread.currentThread().interrupt();
+ }
}
@Override
@@ -435,12 +512,12 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public TrackGroupArray getCurrentTrackGroups() {
- return trackGroups;
+ return playbackInfo.trackSelectorResult.groups;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
- return trackSelections;
+ return playbackInfo.trackSelectorResult.selections;
}
@Override
@@ -456,52 +533,14 @@ import java.util.concurrent.CopyOnWriteArraySet;
// Not private so it can be called from an inner class without going through a thunk method.
/* package */ void handleEvent(Message msg) {
switch (msg.what) {
- case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
- playbackState = msg.arg1;
- for (Player.EventListener listener : listeners) {
- listener.onPlayerStateChanged(playWhenReady, playbackState);
- }
+ case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED:
+ handlePlaybackInfo(
+ (PlaybackInfo) msg.obj,
+ /* operationAcks= */ msg.arg1,
+ /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET,
+ /* positionDiscontinuityReason= */ msg.arg2);
break;
- }
- case ExoPlayerImplInternal.MSG_LOADING_CHANGED: {
- isLoading = msg.arg1 != 0;
- for (Player.EventListener listener : listeners) {
- listener.onLoadingChanged(isLoading);
- }
- break;
- }
- case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
- int prepareAcks = msg.arg1;
- int seekAcks = msg.arg2;
- handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false,
- /* ignored */ DISCONTINUITY_REASON_INTERNAL);
- break;
- }
- case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
- if (pendingPrepareAcks == 0) {
- TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
- tracksSelected = true;
- trackGroups = trackSelectorResult.groups;
- trackSelections = trackSelectorResult.selections;
- trackSelector.onSelectionActivated(trackSelectorResult.info);
- for (Player.EventListener listener : listeners) {
- listener.onTracksChanged(trackGroups, trackSelections);
- }
- }
- break;
- }
- case ExoPlayerImplInternal.MSG_SEEK_ACK: {
- boolean seekPositionAdjusted = msg.arg1 != 0;
- handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted,
- DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
- break;
- }
- case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: {
- @DiscontinuityReason int discontinuityReason = msg.arg1;
- handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason);
- break;
- }
- case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: {
+ case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED:
PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj;
if (!this.playbackParameters.equals(playbackParameters)) {
this.playbackParameters = playbackParameters;
@@ -510,46 +549,123 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
}
break;
- }
- case ExoPlayerImplInternal.MSG_ERROR: {
+ case ExoPlayerImplInternal.MSG_ERROR:
ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
for (Player.EventListener listener : listeners) {
listener.onPlayerError(exception);
}
break;
- }
default:
throw new IllegalStateException();
}
}
- private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks,
- boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) {
- Assertions.checkNotNull(playbackInfo.timeline);
- pendingPrepareAcks -= prepareAcks;
- pendingSeekAcks -= seekAcks;
- if (pendingPrepareAcks == 0 && pendingSeekAcks == 0) {
- boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline
- || this.playbackInfo.manifest != playbackInfo.manifest;
- this.playbackInfo = playbackInfo;
- if (playbackInfo.timeline.isEmpty()) {
- // Update the masking variables, which are used when the timeline is empty.
+ private void handlePlaybackInfo(
+ PlaybackInfo playbackInfo,
+ int operationAcks,
+ boolean positionDiscontinuity,
+ @DiscontinuityReason int positionDiscontinuityReason) {
+ pendingOperationAcks -= operationAcks;
+ if (pendingOperationAcks == 0) {
+ if (playbackInfo.timeline == null) {
+ // Replace internal null timeline with externally visible empty timeline.
+ playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest);
+ }
+ if (playbackInfo.startPositionUs == C.TIME_UNSET) {
+ // Replace internal unset start position with externally visible start position of zero.
+ playbackInfo =
+ playbackInfo.fromNewPosition(
+ playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs);
+ }
+ if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare)
+ && playbackInfo.timeline.isEmpty()) {
+ // Update the masking variables, which are used when the timeline becomes empty.
maskingPeriodIndex = 0;
maskingWindowIndex = 0;
maskingWindowPositionMs = 0;
}
- if (timelineOrManifestChanged) {
- for (Player.EventListener listener : listeners) {
- listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest);
- }
- }
- if (positionDiscontinuity) {
- for (Player.EventListener listener : listeners) {
- listener.onPositionDiscontinuity(positionDiscontinuityReason);
- }
+ @Player.TimelineChangeReason
+ int timelineChangeReason =
+ hasPendingPrepare
+ ? Player.TIMELINE_CHANGE_REASON_PREPARED
+ : Player.TIMELINE_CHANGE_REASON_DYNAMIC;
+ boolean seekProcessed = hasPendingSeek;
+ hasPendingPrepare = false;
+ hasPendingSeek = false;
+ updatePlaybackInfo(
+ playbackInfo,
+ positionDiscontinuity,
+ positionDiscontinuityReason,
+ timelineChangeReason,
+ seekProcessed);
+ }
+ }
+
+ private PlaybackInfo getResetPlaybackInfo(
+ boolean resetPosition, boolean resetState, int playbackState) {
+ if (resetPosition) {
+ maskingWindowIndex = 0;
+ maskingPeriodIndex = 0;
+ maskingWindowPositionMs = 0;
+ } else {
+ maskingWindowIndex = getCurrentWindowIndex();
+ maskingPeriodIndex = getCurrentPeriodIndex();
+ maskingWindowPositionMs = getCurrentPosition();
+ }
+ return new PlaybackInfo(
+ resetState ? Timeline.EMPTY : playbackInfo.timeline,
+ resetState ? null : playbackInfo.manifest,
+ playbackInfo.periodId,
+ playbackInfo.startPositionUs,
+ playbackInfo.contentPositionUs,
+ playbackState,
+ /* isLoading= */ false,
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
+ }
+
+ private void updatePlaybackInfo(
+ PlaybackInfo newPlaybackInfo,
+ boolean positionDiscontinuity,
+ @Player.DiscontinuityReason int positionDiscontinuityReason,
+ @Player.TimelineChangeReason int timelineChangeReason,
+ boolean seekProcessed) {
+ boolean timelineOrManifestChanged =
+ playbackInfo.timeline != newPlaybackInfo.timeline
+ || playbackInfo.manifest != newPlaybackInfo.manifest;
+ boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState;
+ boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading;
+ boolean trackSelectorResultChanged =
+ this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult;
+ playbackInfo = newPlaybackInfo;
+ if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) {
+ for (Player.EventListener listener : listeners) {
+ listener.onTimelineChanged(
+ playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason);
}
}
- if (pendingSeekAcks == 0 && seekAcks > 0) {
+ if (positionDiscontinuity) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPositionDiscontinuity(positionDiscontinuityReason);
+ }
+ }
+ if (trackSelectorResultChanged) {
+ trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info);
+ for (Player.EventListener listener : listeners) {
+ listener.onTracksChanged(
+ playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections);
+ }
+ }
+ if (isLoadingChanged) {
+ for (Player.EventListener listener : listeners) {
+ listener.onLoadingChanged(playbackInfo.isLoading);
+ }
+ }
+ if (playbackStateChanged) {
+ for (Player.EventListener listener : listeners) {
+ listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState);
+ }
+ }
+ if (seekProcessed) {
for (Player.EventListener listener : listeners) {
listener.onSeekProcessed();
}
@@ -566,7 +682,6 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
private boolean shouldMaskPosition() {
- return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareAcks > 0;
+ return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0;
}
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
index 33889a2b57..e05068a7b3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java
@@ -22,43 +22,42 @@ import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
-import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
-import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo;
-import com.google.android.exoplayer2.source.ClippingMediaPeriod;
-import com.google.android.exoplayer2.source.EmptySampleStream;
+import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener;
+import com.google.android.exoplayer2.Player.DiscontinuityReason;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.trackselection.TrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions;
-import com.google.android.exoplayer2.util.MediaClock;
-import com.google.android.exoplayer2.util.StandaloneMediaClock;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.TraceUtil;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
-/**
- * Implements the internal behavior of {@link ExoPlayerImpl}.
- */
-/* package */ final class ExoPlayerImplInternal implements Handler.Callback,
- MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener {
+/** Implements the internal behavior of {@link ExoPlayerImpl}. */
+/* package */ final class ExoPlayerImplInternal
+ implements Handler.Callback,
+ MediaPeriod.Callback,
+ TrackSelector.InvalidationListener,
+ MediaSource.Listener,
+ PlaybackParameterListener,
+ PlayerMessage.Sender {
private static final String TAG = "ExoPlayerImplInternal";
// External messages
- public static final int MSG_STATE_CHANGED = 0;
- public static final int MSG_LOADING_CHANGED = 1;
- public static final int MSG_TRACKS_CHANGED = 2;
- public static final int MSG_SEEK_ACK = 3;
- public static final int MSG_POSITION_DISCONTINUITY = 4;
- public static final int MSG_SOURCE_INFO_REFRESHED = 5;
- public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 6;
- public static final int MSG_ERROR = 7;
+ public static final int MSG_PLAYBACK_INFO_CHANGED = 0;
+ public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1;
+ public static final int MSG_ERROR = 2;
// Internal messages
private static final int MSG_PREPARE = 0;
@@ -66,27 +65,22 @@ import java.io.IOException;
private static final int MSG_DO_SOME_WORK = 2;
private static final int MSG_SEEK_TO = 3;
private static final int MSG_SET_PLAYBACK_PARAMETERS = 4;
- private static final int MSG_STOP = 5;
- private static final int MSG_RELEASE = 6;
- private static final int MSG_REFRESH_SOURCE_INFO = 7;
- private static final int MSG_PERIOD_PREPARED = 8;
- private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 9;
- private static final int MSG_TRACK_SELECTION_INVALIDATED = 10;
- private static final int MSG_CUSTOM = 11;
+ private static final int MSG_SET_SEEK_PARAMETERS = 5;
+ private static final int MSG_STOP = 6;
+ private static final int MSG_RELEASE = 7;
+ private static final int MSG_REFRESH_SOURCE_INFO = 8;
+ private static final int MSG_PERIOD_PREPARED = 9;
+ private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10;
+ private static final int MSG_TRACK_SELECTION_INVALIDATED = 11;
private static final int MSG_SET_REPEAT_MODE = 12;
private static final int MSG_SET_SHUFFLE_ENABLED = 13;
+ private static final int MSG_SEND_MESSAGE = 14;
+ private static final int MSG_SEND_MESSAGE_TO_TARGET_THREAD = 15;
private static final int PREPARING_SOURCE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
- /**
- * Limits the maximum number of periods to buffer ahead of the current playing period. The
- * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
- * small periods to be buffered if the period count were not limited.
- */
- private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
-
/**
* Offset added to all sample timestamps read by renderers to make them non-negative. This is
* provided for convenience of sources that may return negative timestamps due to prerolling
@@ -98,75 +92,88 @@ import java.io.IOException;
private final Renderer[] renderers;
private final RendererCapabilities[] rendererCapabilities;
private final TrackSelector trackSelector;
+ private final TrackSelectorResult emptyTrackSelectorResult;
private final LoadControl loadControl;
- private final StandaloneMediaClock standaloneMediaClock;
- private final Handler handler;
+ private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private final ExoPlayer player;
private final Timeline.Window window;
private final Timeline.Period period;
- private final MediaPeriodInfoSequence mediaPeriodInfoSequence;
+ private final long backBufferDurationUs;
+ private final boolean retainBackBufferFromKeyframe;
+ private final DefaultMediaClock mediaClock;
+ private final PlaybackInfoUpdate playbackInfoUpdate;
+ private final ArrayList pendingMessages;
+ private final Clock clock;
+ private final MediaPeriodQueue queue;
+
+ @SuppressWarnings("unused")
+ private SeekParameters seekParameters;
private PlaybackInfo playbackInfo;
- private PlaybackParameters playbackParameters;
- private Renderer rendererMediaClockSource;
- private MediaClock rendererMediaClock;
private MediaSource mediaSource;
private Renderer[] enabledRenderers;
private boolean released;
private boolean playWhenReady;
private boolean rebuffering;
- private boolean isLoading;
- private int state;
- private @Player.RepeatMode int repeatMode;
+ @Player.RepeatMode private int repeatMode;
private boolean shuffleModeEnabled;
- private int customMessagesSent;
- private int customMessagesProcessed;
- private long elapsedRealtimeUs;
private int pendingPrepareCount;
- private int pendingInitialSeekCount;
- private SeekPosition pendingSeekPosition;
+ private SeekPosition pendingInitialSeekPosition;
private long rendererPositionUs;
+ private int nextPendingMessageIndex;
- private MediaPeriodHolder loadingPeriodHolder;
- private MediaPeriodHolder readingPeriodHolder;
- private MediaPeriodHolder playingPeriodHolder;
-
- public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector,
- LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode,
- boolean shuffleModeEnabled, Handler eventHandler, ExoPlayer player) {
+ public ExoPlayerImplInternal(
+ Renderer[] renderers,
+ TrackSelector trackSelector,
+ TrackSelectorResult emptyTrackSelectorResult,
+ LoadControl loadControl,
+ boolean playWhenReady,
+ @Player.RepeatMode int repeatMode,
+ boolean shuffleModeEnabled,
+ Handler eventHandler,
+ ExoPlayer player,
+ Clock clock) {
this.renderers = renderers;
this.trackSelector = trackSelector;
+ this.emptyTrackSelectorResult = emptyTrackSelectorResult;
this.loadControl = loadControl;
this.playWhenReady = playWhenReady;
this.repeatMode = repeatMode;
this.shuffleModeEnabled = shuffleModeEnabled;
this.eventHandler = eventHandler;
- this.state = Player.STATE_IDLE;
this.player = player;
+ this.clock = clock;
+ this.queue = new MediaPeriodQueue();
- playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET);
+ backBufferDurationUs = loadControl.getBackBufferDurationUs();
+ retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe();
+
+ seekParameters = SeekParameters.DEFAULT;
+ playbackInfo =
+ new PlaybackInfo(
+ /* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult);
+ playbackInfoUpdate = new PlaybackInfoUpdate();
rendererCapabilities = new RendererCapabilities[renderers.length];
for (int i = 0; i < renderers.length; i++) {
renderers[i].setIndex(i);
rendererCapabilities[i] = renderers[i].getCapabilities();
}
- standaloneMediaClock = new StandaloneMediaClock();
+ mediaClock = new DefaultMediaClock(this, clock);
+ pendingMessages = new ArrayList<>();
enabledRenderers = new Renderer[0];
window = new Timeline.Window();
period = new Timeline.Period();
- mediaPeriodInfoSequence = new MediaPeriodInfoSequence();
trackSelector.init(this);
- playbackParameters = PlaybackParameters.DEFAULT;
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
- handler = new Handler(internalPlaybackThread.getLooper(), this);
+ handler = clock.createHandler(internalPlaybackThread.getLooper(), this);
}
public void prepare(MediaSource mediaSource, boolean resetPosition) {
@@ -195,38 +202,22 @@ import java.io.IOException;
handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget();
}
- public void stop() {
- handler.sendEmptyMessage(MSG_STOP);
+ public void setSeekParameters(SeekParameters seekParameters) {
+ handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget();
}
- public void sendMessages(ExoPlayerMessage... messages) {
- if (released) {
- Log.w(TAG, "Ignoring messages sent after release.");
- return;
- }
- customMessagesSent++;
- handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
+ public void stop(boolean reset) {
+ handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget();
}
- public synchronized void blockingSendMessages(ExoPlayerMessage... messages) {
+ @Override
+ public synchronized void sendMessage(PlayerMessage message) {
if (released) {
Log.w(TAG, "Ignoring messages sent after release.");
+ message.markAsProcessed(/* isDelivered= */ false);
return;
}
- int messageNumber = customMessagesSent++;
- handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget();
- boolean wasInterrupted = false;
- while (customMessagesProcessed <= messageNumber) {
- try {
- wait();
- } catch (InterruptedException e) {
- wasInterrupted = true;
- }
- }
- if (wasInterrupted) {
- // Restore the interrupted status.
- Thread.currentThread().interrupt();
- }
+ handler.obtainMessage(MSG_SEND_MESSAGE, message).sendToTarget();
}
public synchronized void release() {
@@ -279,6 +270,14 @@ import java.io.IOException;
handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED);
}
+ // DefaultMediaClock.PlaybackParameterListener implementation.
+
+ @Override
+ public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
+ eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
+ }
+
// Handler.Callback implementation.
@SuppressWarnings("unchecked")
@@ -286,114 +285,115 @@ import java.io.IOException;
public boolean handleMessage(Message msg) {
try {
switch (msg.what) {
- case MSG_PREPARE: {
+ case MSG_PREPARE:
prepareInternal((MediaSource) msg.obj, msg.arg1 != 0);
- return true;
- }
- case MSG_SET_PLAY_WHEN_READY: {
+ break;
+ case MSG_SET_PLAY_WHEN_READY:
setPlayWhenReadyInternal(msg.arg1 != 0);
- return true;
- }
- case MSG_SET_REPEAT_MODE: {
+ break;
+ case MSG_SET_REPEAT_MODE:
setRepeatModeInternal(msg.arg1);
- return true;
- }
- case MSG_SET_SHUFFLE_ENABLED: {
+ break;
+ case MSG_SET_SHUFFLE_ENABLED:
setShuffleModeEnabledInternal(msg.arg1 != 0);
- return true;
- }
- case MSG_DO_SOME_WORK: {
+ break;
+ case MSG_DO_SOME_WORK:
doSomeWork();
- return true;
- }
- case MSG_SEEK_TO: {
+ break;
+ case MSG_SEEK_TO:
seekToInternal((SeekPosition) msg.obj);
- return true;
- }
- case MSG_SET_PLAYBACK_PARAMETERS: {
+ break;
+ case MSG_SET_PLAYBACK_PARAMETERS:
setPlaybackParametersInternal((PlaybackParameters) msg.obj);
- return true;
- }
- case MSG_STOP: {
- stopInternal();
- return true;
- }
- case MSG_RELEASE: {
- releaseInternal();
- return true;
- }
- case MSG_PERIOD_PREPARED: {
+ break;
+ case MSG_SET_SEEK_PARAMETERS:
+ setSeekParametersInternal((SeekParameters) msg.obj);
+ break;
+ case MSG_STOP:
+ stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true);
+ break;
+ case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj);
- return true;
- }
- case MSG_REFRESH_SOURCE_INFO: {
+ break;
+ case MSG_REFRESH_SOURCE_INFO:
handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj);
- return true;
- }
- case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: {
+ break;
+ case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
handleContinueLoadingRequested((MediaPeriod) msg.obj);
- return true;
- }
- case MSG_TRACK_SELECTION_INVALIDATED: {
+ break;
+ case MSG_TRACK_SELECTION_INVALIDATED:
reselectTracksInternal();
+ break;
+ case MSG_SEND_MESSAGE:
+ sendMessageInternal((PlayerMessage) msg.obj);
+ break;
+ case MSG_SEND_MESSAGE_TO_TARGET_THREAD:
+ sendMessageToTargetThread((PlayerMessage) msg.obj);
+ break;
+ case MSG_RELEASE:
+ releaseInternal();
+ // Return immediately to not send playback info updates after release.
return true;
- }
- case MSG_CUSTOM: {
- sendMessagesInternal((ExoPlayerMessage[]) msg.obj);
- return true;
- }
default:
return false;
}
+ maybeNotifyPlaybackInfoChanged();
} catch (ExoPlaybackException e) {
Log.e(TAG, "Renderer error.", e);
+ stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
- stopInternal();
- return true;
+ maybeNotifyPlaybackInfoChanged();
} catch (IOException e) {
Log.e(TAG, "Source error.", e);
+ stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget();
- stopInternal();
- return true;
+ maybeNotifyPlaybackInfoChanged();
} catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e);
+ stopInternal(/* reset= */ false, /* acknowledgeStop= */ false);
eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e))
.sendToTarget();
- stopInternal();
- return true;
+ maybeNotifyPlaybackInfoChanged();
}
+ return true;
}
// Private methods.
private void setState(int state) {
- if (this.state != state) {
- this.state = state;
- eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+ if (playbackInfo.playbackState != state) {
+ playbackInfo = playbackInfo.copyWithPlaybackState(state);
}
}
private void setIsLoading(boolean isLoading) {
- if (this.isLoading != isLoading) {
- this.isLoading = isLoading;
- eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget();
+ if (playbackInfo.isLoading != isLoading) {
+ playbackInfo = playbackInfo.copyWithIsLoading(isLoading);
+ }
+ }
+
+ private void maybeNotifyPlaybackInfoChanged() {
+ if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) {
+ eventHandler
+ .obtainMessage(
+ MSG_PLAYBACK_INFO_CHANGED,
+ playbackInfoUpdate.operationAcks,
+ playbackInfoUpdate.positionDiscontinuity
+ ? playbackInfoUpdate.discontinuityReason
+ : C.INDEX_UNSET,
+ playbackInfo)
+ .sendToTarget();
+ playbackInfoUpdate.reset(playbackInfo);
}
}
private void prepareInternal(MediaSource mediaSource, boolean resetPosition) {
pendingPrepareCount++;
- resetInternal(true);
+ resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true);
loadControl.onPrepared();
- if (resetPosition) {
- playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET);
- } else {
- // The new start position is the current playback position.
- playbackInfo = new PlaybackInfo(null, null, playbackInfo.periodId, playbackInfo.positionUs,
- playbackInfo.contentPositionUs);
- }
this.mediaSource = mediaSource;
- mediaSource.prepareSource(player, true, this);
setState(Player.STATE_BUFFERING);
+ mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this);
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
@@ -404,10 +404,10 @@ import java.io.IOException;
stopRenderers();
updatePlaybackPositions();
} else {
- if (state == Player.STATE_READY) {
+ if (playbackInfo.playbackState == Player.STATE_READY) {
startRenderers();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
- } else if (state == Player.STATE_BUFFERING) {
+ } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
@@ -416,149 +416,107 @@ import java.io.IOException;
private void setRepeatModeInternal(@Player.RepeatMode int repeatMode)
throws ExoPlaybackException {
this.repeatMode = repeatMode;
- mediaPeriodInfoSequence.setRepeatMode(repeatMode);
- validateExistingPeriodHolders();
+ if (!queue.updateRepeatMode(repeatMode)) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ true);
+ }
}
private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled)
throws ExoPlaybackException {
this.shuffleModeEnabled = shuffleModeEnabled;
- mediaPeriodInfoSequence.setShuffleModeEnabled(shuffleModeEnabled);
- validateExistingPeriodHolders();
+ if (!queue.updateShuffleModeEnabled(shuffleModeEnabled)) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ true);
+ }
}
- private void validateExistingPeriodHolders() throws ExoPlaybackException {
- // Find the last existing period holder that matches the new period order.
- MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null
- ? playingPeriodHolder : loadingPeriodHolder;
- if (lastValidPeriodHolder == null) {
- return;
- }
- while (true) {
- int nextPeriodIndex = playbackInfo.timeline.getNextPeriodIndex(
- lastValidPeriodHolder.info.id.periodIndex, period, window, repeatMode,
- shuffleModeEnabled);
- while (lastValidPeriodHolder.next != null
- && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
- lastValidPeriodHolder = lastValidPeriodHolder.next;
- }
- if (nextPeriodIndex == C.INDEX_UNSET || lastValidPeriodHolder.next == null
- || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) {
- break;
- }
- lastValidPeriodHolder = lastValidPeriodHolder.next;
- }
-
- // Release any period holders that don't match the new period order.
- int loadingPeriodHolderIndex = loadingPeriodHolder.index;
- int readingPeriodHolderIndex =
- readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET;
- if (lastValidPeriodHolder.next != null) {
- releasePeriodHoldersFrom(lastValidPeriodHolder.next);
- lastValidPeriodHolder.next = null;
- }
-
- // Update the period info for the last holder, as it may now be the last period in the timeline.
- lastValidPeriodHolder.info =
- mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info);
-
- // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed.
- boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index;
- if (!seenLoadingPeriodHolder) {
- loadingPeriodHolder = lastValidPeriodHolder;
- }
- boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET
- && readingPeriodHolderIndex <= lastValidPeriodHolder.index;
- if (!seenReadingPeriodHolder && playingPeriodHolder != null) {
- // Renderers may have read from a period that's been removed. Seek back to the current
- // position of the playing period to make sure none of the removed period is played.
- MediaPeriodId periodId = playingPeriodHolder.info.id;
- long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs);
- if (newPositionUs != playbackInfo.positionUs) {
- playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs,
- playbackInfo.contentPositionUs);
- eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL,
- 0, playbackInfo).sendToTarget();
+ private void seekToCurrentPosition(boolean sendDiscontinuity) throws ExoPlaybackException {
+ // Renderers may have read from a period that's been removed. Seek back to the current
+ // position of the playing period to make sure none of the removed period is played.
+ MediaPeriodId periodId = queue.getPlayingPeriod().info.id;
+ long newPositionUs =
+ seekToPeriodPosition(periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true);
+ if (newPositionUs != playbackInfo.positionUs) {
+ playbackInfo =
+ playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs);
+ if (sendDiscontinuity) {
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
}
}
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
- standaloneMediaClock.start();
+ mediaClock.start();
for (Renderer renderer : enabledRenderers) {
renderer.start();
}
}
private void stopRenderers() throws ExoPlaybackException {
- standaloneMediaClock.stop();
+ mediaClock.stop();
for (Renderer renderer : enabledRenderers) {
ensureStopped(renderer);
}
}
private void updatePlaybackPositions() throws ExoPlaybackException {
- if (playingPeriodHolder == null) {
+ if (!queue.hasPlayingPeriod()) {
return;
}
// Update the playback position.
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity();
if (periodPositionUs != C.TIME_UNSET) {
resetRendererPosition(periodPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
- playbackInfo.contentPositionUs);
- eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL,
- 0, playbackInfo).sendToTarget();
- } else {
- // Use the standalone clock if there's no renderer clock, or if the providing renderer has
- // ended or needs the next sample stream to reenter the ready state. The latter case uses the
- // standalone clock to avoid getting stuck if tracks in the current period have uneven
- // durations. See: https://github.com/google/ExoPlayer/issues/1874.
- if (rendererMediaClockSource == null || rendererMediaClockSource.isEnded()
- || (!rendererMediaClockSource.isReady()
- && rendererWaitingForNextStream(rendererMediaClockSource))) {
- rendererPositionUs = standaloneMediaClock.getPositionUs();
- } else {
- rendererPositionUs = rendererMediaClock.getPositionUs();
- standaloneMediaClock.setPositionUs(rendererPositionUs);
+ // A MediaPeriod may report a discontinuity at the current playback position to ensure the
+ // renderers are flushed. Only report the discontinuity externally if the position changed.
+ if (periodPositionUs != playbackInfo.positionUs) {
+ playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
+ playbackInfo.contentPositionUs);
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
}
+ } else {
+ rendererPositionUs = mediaClock.syncAndGetPositionUs();
periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs);
+ maybeTriggerPendingMessages(playbackInfo.positionUs, periodPositionUs);
+ playbackInfo.positionUs = periodPositionUs;
}
- playbackInfo.positionUs = periodPositionUs;
- elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
// Update the buffered position.
- long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE
- : playingPeriodHolder.mediaPeriod.getBufferedPositionUs();
- playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE
- ? playingPeriodHolder.info.durationUs : bufferedPositionUs;
+ playbackInfo.bufferedPositionUs =
+ enabledRenderers.length == 0
+ ? playingPeriodHolder.info.durationUs
+ : playingPeriodHolder.getBufferedPositionUs(/* convertEosToDuration= */ true);
}
private void doSomeWork() throws ExoPlaybackException, IOException {
- long operationStartTimeMs = SystemClock.elapsedRealtime();
+ long operationStartTimeMs = clock.uptimeMillis();
updatePeriods();
- if (playingPeriodHolder == null) {
+ if (!queue.hasPlayingPeriod()) {
// We're still waiting for the first period to be prepared.
maybeThrowPeriodPrepareError();
scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS);
return;
}
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions();
- playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
+ long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
- boolean allRenderersEnded = true;
- boolean allRenderersReadyOrEnded = true;
+ playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs,
+ retainBackBufferFromKeyframe);
+
+ boolean renderersEnded = true;
+ boolean renderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) {
// TODO: Each renderer should return the maximum delay before which it wishes to be called
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
- renderer.render(rendererPositionUs, elapsedRealtimeUs);
- allRenderersEnded = allRenderersEnded && renderer.isEnded();
+ renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
+ renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer is ready (or ended). We override to assume the renderer is
// ready if it needs the next sample stream. This is necessary to avoid getting stuck if
// tracks in the current period have uneven durations. See:
@@ -568,63 +526,42 @@ import java.io.IOException;
if (!rendererReadyOrEnded) {
renderer.maybeThrowStreamError();
}
- allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded;
+ renderersReadyOrEnded = renderersReadyOrEnded && rendererReadyOrEnded;
}
-
- if (!allRenderersReadyOrEnded) {
+ if (!renderersReadyOrEnded) {
maybeThrowPeriodPrepareError();
}
- // The standalone media clock never changes playback parameters, so just check the renderer.
- if (rendererMediaClock != null) {
- PlaybackParameters playbackParameters = rendererMediaClock.getPlaybackParameters();
- if (!playbackParameters.equals(this.playbackParameters)) {
- // TODO: Make LoadControl, period transition position projection, adaptive track selection
- // and potentially any time-related code in renderers take into account the playback speed.
- this.playbackParameters = playbackParameters;
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
- eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters)
- .sendToTarget();
- }
- }
-
long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
- if (allRenderersEnded
+ if (renderersEnded
&& (playingPeriodDurationUs == C.TIME_UNSET
- || playingPeriodDurationUs <= playbackInfo.positionUs)
+ || playingPeriodDurationUs <= playbackInfo.positionUs)
&& playingPeriodHolder.info.isFinal) {
setState(Player.STATE_ENDED);
stopRenderers();
- } else if (state == Player.STATE_BUFFERING) {
- boolean isNewlyReady = enabledRenderers.length > 0
- ? (allRenderersReadyOrEnded
- && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs))
- : isTimelineReady(playingPeriodDurationUs);
- if (isNewlyReady) {
- setState(Player.STATE_READY);
- if (playWhenReady) {
- startRenderers();
- }
- }
- } else if (state == Player.STATE_READY) {
- boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded
- : isTimelineReady(playingPeriodDurationUs);
- if (!isStillReady) {
- rebuffering = playWhenReady;
- setState(Player.STATE_BUFFERING);
- stopRenderers();
+ } else if (playbackInfo.playbackState == Player.STATE_BUFFERING
+ && shouldTransitionToReadyState(renderersReadyOrEnded)) {
+ setState(Player.STATE_READY);
+ if (playWhenReady) {
+ startRenderers();
}
+ } else if (playbackInfo.playbackState == Player.STATE_READY
+ && !(enabledRenderers.length == 0 ? isTimelineReady() : renderersReadyOrEnded)) {
+ rebuffering = playWhenReady;
+ setState(Player.STATE_BUFFERING);
+ stopRenderers();
}
- if (state == Player.STATE_BUFFERING) {
+ if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
for (Renderer renderer : enabledRenderers) {
renderer.maybeThrowStreamError();
}
}
- if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) {
+ if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY)
+ || playbackInfo.playbackState == Player.STATE_BUFFERING) {
scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS);
- } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) {
+ } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
} else {
handler.removeMessages(MSG_DO_SOME_WORK);
@@ -635,118 +572,123 @@ import java.io.IOException;
private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) {
handler.removeMessages(MSG_DO_SOME_WORK);
- long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
- long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
- if (nextOperationDelayMs <= 0) {
- handler.sendEmptyMessage(MSG_DO_SOME_WORK);
- } else {
- handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs);
- }
+ handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
}
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
Timeline timeline = playbackInfo.timeline;
- if (timeline == null) {
- pendingInitialSeekCount++;
- pendingSeekPosition = seekPosition;
- return;
- }
+ playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
- Pair periodPosition = resolveSeekPosition(seekPosition);
- if (periodPosition == null) {
- int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow(
- timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex;
+ MediaPeriodId periodId;
+ long periodPositionUs;
+ long contentPositionUs;
+ boolean seekPositionAdjusted;
+ Pair resolvedSeekPosition =
+ resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true);
+ if (resolvedSeekPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
- // timeline has changed and a suitable seek position could not be resolved in the new one.
- // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to
- // (firstPeriodIndex,0) isn't ignored.
- playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET);
- setState(Player.STATE_ENDED);
- eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0,
- playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)).sendToTarget();
- // Reset, but retain the source so that it can still be used should a seek occur.
- resetInternal(false);
- return;
+ // timeline has changed or is not ready and a suitable seek position could not be resolved.
+ periodId = new MediaPeriodId(getFirstPeriodIndex());
+ periodPositionUs = C.TIME_UNSET;
+ contentPositionUs = C.TIME_UNSET;
+ seekPositionAdjusted = true;
+ } else {
+ // Update the resolved seek position to take ads into account.
+ int periodIndex = resolvedSeekPosition.first;
+ contentPositionUs = resolvedSeekPosition.second;
+ periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs);
+ if (periodId.isAd()) {
+ periodPositionUs = 0;
+ seekPositionAdjusted = true;
+ } else {
+ periodPositionUs = resolvedSeekPosition.second;
+ seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
+ }
}
- boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
- int periodIndex = periodPosition.first;
- long periodPositionUs = periodPosition.second;
- long contentPositionUs = periodPositionUs;
- MediaPeriodId periodId =
- mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs);
- if (periodId.isAd()) {
- seekPositionAdjusted = true;
- periodPositionUs = 0;
- }
try {
- if (periodId.equals(playbackInfo.periodId)
- && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) {
- // Seek position equals the current position. Do nothing.
- return;
+ if (mediaSource == null || timeline == null) {
+ // Save seek position for later, as we are still waiting for a prepared source.
+ pendingInitialSeekPosition = seekPosition;
+ } else if (periodPositionUs == C.TIME_UNSET) {
+ // End playback, as we didn't manage to find a valid seek position.
+ setState(Player.STATE_ENDED);
+ resetInternal(
+ /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false);
+ } else {
+ // Execute the seek in the current media periods.
+ long newPeriodPositionUs = periodPositionUs;
+ if (periodId.equals(playbackInfo.periodId)) {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ if (playingPeriodHolder != null && newPeriodPositionUs != 0) {
+ newPeriodPositionUs =
+ playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs(
+ newPeriodPositionUs, seekParameters);
+ }
+ if (C.usToMs(newPeriodPositionUs) == C.usToMs(playbackInfo.positionUs)) {
+ // Seek will be performed to the current position. Do nothing.
+ periodPositionUs = playbackInfo.positionUs;
+ return;
+ }
+ }
+ newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs);
+ seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
+ periodPositionUs = newPeriodPositionUs;
}
- long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs);
- seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
- periodPositionUs = newPeriodPositionUs;
} finally {
playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs);
- eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
- .sendToTarget();
+ if (seekPositionAdjusted) {
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT);
+ }
}
}
private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs)
throws ExoPlaybackException {
+ // Force disable renderers if they are reading from a period other than the one being played.
+ return seekToPeriodPosition(
+ periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod());
+ }
+
+ private long seekToPeriodPosition(
+ MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers)
+ throws ExoPlaybackException {
stopRenderers();
rebuffering = false;
setState(Player.STATE_BUFFERING);
- MediaPeriodHolder newPlayingPeriodHolder = null;
- if (playingPeriodHolder == null) {
- // We're still waiting for the first period to be prepared.
- if (loadingPeriodHolder != null) {
- loadingPeriodHolder.release();
- }
- } else {
- // Clear the timeline, but keep the requested period if it is already prepared.
- MediaPeriodHolder periodHolder = playingPeriodHolder;
- while (periodHolder != null) {
- if (newPlayingPeriodHolder == null
- && shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) {
- newPlayingPeriodHolder = periodHolder;
- } else {
- periodHolder.release();
- }
- periodHolder = periodHolder.next;
+ // Clear the timeline, but keep the requested period if it is already prepared.
+ MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder;
+ while (newPlayingPeriodHolder != null) {
+ if (shouldKeepPeriodHolder(periodId, periodPositionUs, newPlayingPeriodHolder)) {
+ queue.removeAfter(newPlayingPeriodHolder);
+ break;
}
+ newPlayingPeriodHolder = queue.advancePlayingPeriod();
}
- // Disable all the renderers if the period being played is changing, or if the renderers are
- // reading from a period other than the one being played.
- if (playingPeriodHolder != newPlayingPeriodHolder
- || playingPeriodHolder != readingPeriodHolder) {
+ // Disable all the renderers if the period being played is changing, or if forced.
+ if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) {
for (Renderer renderer : enabledRenderers) {
disableRenderer(renderer);
}
enabledRenderers = new Renderer[0];
- playingPeriodHolder = null;
+ oldPlayingPeriodHolder = null;
}
// Update the holders.
if (newPlayingPeriodHolder != null) {
- newPlayingPeriodHolder.next = null;
- loadingPeriodHolder = newPlayingPeriodHolder;
- readingPeriodHolder = newPlayingPeriodHolder;
- setPlayingPeriodHolder(newPlayingPeriodHolder);
- if (playingPeriodHolder.hasEnabledTracks) {
- periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
+ if (newPlayingPeriodHolder.hasEnabledTracks) {
+ periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs);
+ newPlayingPeriodHolder.mediaPeriod.discardBuffer(
+ periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
}
resetRendererPosition(periodPositionUs);
maybeContinueLoading();
} else {
- loadingPeriodHolder = null;
- readingPeriodHolder = null;
- playingPeriodHolder = null;
+ queue.clear();
resetRendererPosition(periodPositionUs);
}
@@ -754,8 +696,8 @@ import java.io.IOException;
return periodPositionUs;
}
- private boolean shouldKeepPeriodHolder(MediaPeriodId seekPeriodId, long positionUs,
- MediaPeriodHolder holder) {
+ private boolean shouldKeepPeriodHolder(
+ MediaPeriodId seekPeriodId, long positionUs, MediaPeriodHolder holder) {
if (seekPeriodId.equals(holder.info.id) && holder.prepared) {
playbackInfo.timeline.getPeriod(holder.info.id.periodIndex, period);
int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs);
@@ -768,32 +710,37 @@ import java.io.IOException;
}
private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException {
- rendererPositionUs = playingPeriodHolder == null
- ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
- : playingPeriodHolder.toRendererTime(periodPositionUs);
- standaloneMediaClock.setPositionUs(rendererPositionUs);
+ rendererPositionUs =
+ !queue.hasPlayingPeriod()
+ ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US
+ : queue.getPlayingPeriod().toRendererTime(periodPositionUs);
+ mediaClock.resetPosition(rendererPositionUs);
for (Renderer renderer : enabledRenderers) {
renderer.resetPosition(rendererPositionUs);
}
}
private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) {
- if (rendererMediaClock != null) {
- playbackParameters = rendererMediaClock.setPlaybackParameters(playbackParameters);
- }
- standaloneMediaClock.setPlaybackParameters(playbackParameters);
- this.playbackParameters = playbackParameters;
- eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget();
+ mediaClock.setPlaybackParameters(playbackParameters);
}
- private void stopInternal() {
- resetInternal(true);
+ private void setSeekParametersInternal(SeekParameters seekParameters) {
+ this.seekParameters = seekParameters;
+ }
+
+ private void stopInternal(boolean reset, boolean acknowledgeStop) {
+ resetInternal(
+ /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset);
+ playbackInfoUpdate.incrementPendingOperationAcks(
+ pendingPrepareCount + (acknowledgeStop ? 1 : 0));
+ pendingPrepareCount = 0;
loadControl.onStopped();
setState(Player.STATE_IDLE);
}
private void releaseInternal() {
- resetInternal(true);
+ resetInternal(
+ /* releaseMediaSource= */ true, /* resetPosition= */ true, /* resetState= */ true);
loadControl.onReleased();
setState(Player.STATE_IDLE);
internalPlaybackThread.quit();
@@ -803,10 +750,19 @@ import java.io.IOException;
}
}
- private void resetInternal(boolean releaseMediaSource) {
+ private int getFirstPeriodIndex() {
+ Timeline timeline = playbackInfo.timeline;
+ return timeline == null || timeline.isEmpty()
+ ? 0
+ : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window)
+ .firstPeriodIndex;
+ }
+
+ private void resetInternal(
+ boolean releaseMediaSource, boolean resetPosition, boolean resetState) {
handler.removeMessages(MSG_DO_SOME_WORK);
rebuffering = false;
- standaloneMediaClock.stop();
+ mediaClock.stop();
rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US;
for (Renderer renderer : enabledRenderers) {
try {
@@ -817,37 +773,185 @@ import java.io.IOException;
}
}
enabledRenderers = new Renderer[0];
- releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder
- : loadingPeriodHolder);
- loadingPeriodHolder = null;
- readingPeriodHolder = null;
- playingPeriodHolder = null;
+ queue.clear();
setIsLoading(false);
+ if (resetPosition) {
+ pendingInitialSeekPosition = null;
+ }
+ if (resetState) {
+ queue.setTimeline(null);
+ for (PendingMessageInfo pendingMessageInfo : pendingMessages) {
+ pendingMessageInfo.message.markAsProcessed(/* isDelivered= */ false);
+ }
+ pendingMessages.clear();
+ nextPendingMessageIndex = 0;
+ }
+ playbackInfo =
+ new PlaybackInfo(
+ resetState ? null : playbackInfo.timeline,
+ resetState ? null : playbackInfo.manifest,
+ resetPosition ? new MediaPeriodId(getFirstPeriodIndex()) : playbackInfo.periodId,
+ // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored.
+ resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs,
+ resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs,
+ playbackInfo.playbackState,
+ /* isLoading= */ false,
+ resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult);
if (releaseMediaSource) {
if (mediaSource != null) {
mediaSource.releaseSource();
mediaSource = null;
}
- mediaPeriodInfoSequence.setTimeline(null);
- playbackInfo = playbackInfo.copyWithTimeline(null, null);
}
}
- private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException {
- try {
- for (ExoPlayerMessage message : messages) {
- message.target.handleMessage(message.messageType, message.message);
+ private void sendMessageInternal(PlayerMessage message) {
+ if (message.getPositionMs() == C.TIME_UNSET) {
+ // If no delivery time is specified, trigger immediate message delivery.
+ sendMessageToTarget(message);
+ } else if (playbackInfo.timeline == null) {
+ // Still waiting for initial timeline to resolve position.
+ pendingMessages.add(new PendingMessageInfo(message));
+ } else {
+ PendingMessageInfo pendingMessageInfo = new PendingMessageInfo(message);
+ if (resolvePendingMessagePosition(pendingMessageInfo)) {
+ pendingMessages.add(pendingMessageInfo);
+ // Ensure new message is inserted according to playback order.
+ Collections.sort(pendingMessages);
+ } else {
+ message.markAsProcessed(/* isDelivered= */ false);
}
- if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) {
+ }
+ }
+
+ private void sendMessageToTarget(PlayerMessage message) {
+ if (message.getHandler().getLooper() == handler.getLooper()) {
+ deliverMessage(message);
+ if (playbackInfo.playbackState == Player.STATE_READY
+ || playbackInfo.playbackState == Player.STATE_BUFFERING) {
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
+ } else {
+ handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET_THREAD, message).sendToTarget();
+ }
+ }
+
+ private void sendMessageToTargetThread(final PlayerMessage message) {
+ message
+ .getHandler()
+ .post(
+ new Runnable() {
+ @Override
+ public void run() {
+ deliverMessage(message);
+ }
+ });
+ }
+
+ private void deliverMessage(PlayerMessage message) {
+ try {
+ message.getTarget().handleMessage(message.getType(), message.getPayload());
+ } catch (ExoPlaybackException e) {
+ eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
} finally {
- synchronized (this) {
- customMessagesProcessed++;
- notifyAll();
+ message.markAsProcessed(/* isDelivered= */ true);
+ }
+ }
+
+ private void resolvePendingMessagePositions() {
+ for (int i = pendingMessages.size() - 1; i >= 0; i--) {
+ if (!resolvePendingMessagePosition(pendingMessages.get(i))) {
+ // Unable to resolve a new position for the message. Remove it.
+ pendingMessages.get(i).message.markAsProcessed(/* isDelivered= */ false);
+ pendingMessages.remove(i);
}
}
+ // Re-sort messages by playback order.
+ Collections.sort(pendingMessages);
+ }
+
+ private boolean resolvePendingMessagePosition(PendingMessageInfo pendingMessageInfo) {
+ if (pendingMessageInfo.resolvedPeriodUid == null) {
+ // Position is still unresolved. Try to find window in current timeline.
+ Pair periodPosition =
+ resolveSeekPosition(
+ new SeekPosition(
+ pendingMessageInfo.message.getTimeline(),
+ pendingMessageInfo.message.getWindowIndex(),
+ C.msToUs(pendingMessageInfo.message.getPositionMs())),
+ /* trySubsequentPeriods= */ false);
+ if (periodPosition == null) {
+ return false;
+ }
+ pendingMessageInfo.setResolvedPosition(
+ periodPosition.first,
+ periodPosition.second,
+ playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid);
+ } else {
+ // Position has been resolved for a previous timeline. Try to find the updated period index.
+ int index = playbackInfo.timeline.getIndexOfPeriod(pendingMessageInfo.resolvedPeriodUid);
+ if (index == C.INDEX_UNSET) {
+ return false;
+ }
+ pendingMessageInfo.resolvedPeriodIndex = index;
+ }
+ return true;
+ }
+
+ private void maybeTriggerPendingMessages(long oldPeriodPositionUs, long newPeriodPositionUs) {
+ if (pendingMessages.isEmpty() || playbackInfo.periodId.isAd()) {
+ return;
+ }
+ // If this is the first call from the start position, include oldPeriodPositionUs in potential
+ // trigger positions.
+ if (playbackInfo.startPositionUs == oldPeriodPositionUs) {
+ oldPeriodPositionUs--;
+ }
+ // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages)
+ int currentPeriodIndex = playbackInfo.periodId.periodIndex;
+ PendingMessageInfo previousInfo =
+ nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
+ while (previousInfo != null
+ && (previousInfo.resolvedPeriodIndex > currentPeriodIndex
+ || (previousInfo.resolvedPeriodIndex == currentPeriodIndex
+ && previousInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) {
+ nextPendingMessageIndex--;
+ previousInfo =
+ nextPendingMessageIndex > 0 ? pendingMessages.get(nextPendingMessageIndex - 1) : null;
+ }
+ PendingMessageInfo nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ while (nextInfo != null
+ && nextInfo.resolvedPeriodUid != null
+ && (nextInfo.resolvedPeriodIndex < currentPeriodIndex
+ || (nextInfo.resolvedPeriodIndex == currentPeriodIndex
+ && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) {
+ nextPendingMessageIndex++;
+ nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ }
+ // Check if any message falls within the covered time span.
+ while (nextInfo != null
+ && nextInfo.resolvedPeriodUid != null
+ && nextInfo.resolvedPeriodIndex == currentPeriodIndex
+ && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
+ && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
+ sendMessageToTarget(nextInfo.message);
+ if (nextInfo.message.getDeleteAfterDelivery()) {
+ pendingMessages.remove(nextPendingMessageIndex);
+ } else {
+ nextPendingMessageIndex++;
+ }
+ nextInfo =
+ nextPendingMessageIndex < pendingMessages.size()
+ ? pendingMessages.get(nextPendingMessageIndex)
+ : null;
+ }
}
private void ensureStopped(Renderer renderer) throws ExoPlaybackException {
@@ -857,28 +961,27 @@ import java.io.IOException;
}
private void disableRenderer(Renderer renderer) throws ExoPlaybackException {
- if (renderer == rendererMediaClockSource) {
- rendererMediaClock = null;
- rendererMediaClockSource = null;
- }
+ mediaClock.onRendererDisabled(renderer);
ensureStopped(renderer);
renderer.disable();
}
private void reselectTracksInternal() throws ExoPlaybackException {
- if (playingPeriodHolder == null) {
+ if (!queue.hasPlayingPeriod()) {
// We don't have tracks yet, so we don't care.
return;
}
+ float playbackSpeed = mediaClock.getPlaybackParameters().speed;
// Reselect tracks on each period in turn, until the selection changes.
- MediaPeriodHolder periodHolder = playingPeriodHolder;
+ MediaPeriodHolder periodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
boolean selectionsChangedForReadPeriod = true;
while (true) {
if (periodHolder == null || !periodHolder.prepared) {
// The reselection did not change any prepared periods.
return;
}
- if (periodHolder.selectTracks()) {
+ if (periodHolder.selectTracks(playbackSpeed)) {
// Selected tracks have changed for this period.
break;
}
@@ -891,20 +994,19 @@ import java.io.IOException;
if (selectionsChangedForReadPeriod) {
// Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
- boolean recreateStreams = readingPeriodHolder != playingPeriodHolder;
- releasePeriodHoldersFrom(playingPeriodHolder.next);
- playingPeriodHolder.next = null;
- loadingPeriodHolder = playingPeriodHolder;
- readingPeriodHolder = playingPeriodHolder;
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ boolean recreateStreams = queue.removeAfter(playingPeriodHolder);
boolean[] streamResetFlags = new boolean[renderers.length];
- long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection(
- playbackInfo.positionUs, recreateStreams, streamResetFlags);
- if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) {
+ long periodPositionUs =
+ playingPeriodHolder.applyTrackSelection(
+ playbackInfo.positionUs, recreateStreams, streamResetFlags);
+ updateLoadControlTrackSelection(playingPeriodHolder.trackSelectorResult);
+ if (playbackInfo.playbackState != Player.STATE_ENDED
+ && periodPositionUs != playbackInfo.positionUs) {
playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs,
playbackInfo.contentPositionUs);
- eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL,
- 0, playbackInfo).sendToTarget();
+ playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL);
resetRendererPosition(periodPositionUs);
}
@@ -927,39 +1029,82 @@ import java.io.IOException;
}
}
}
- eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
- .sendToTarget();
+ playbackInfo =
+ playbackInfo.copyWithTrackSelectorResult(playingPeriodHolder.trackSelectorResult);
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else {
// Release and re-prepare/buffer periods after the one whose selection changed.
- loadingPeriodHolder = periodHolder;
- periodHolder = loadingPeriodHolder.next;
- while (periodHolder != null) {
- periodHolder.release();
- periodHolder = periodHolder.next;
- }
- loadingPeriodHolder.next = null;
- if (loadingPeriodHolder.prepared) {
- long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs,
- loadingPeriodHolder.toPeriodTime(rendererPositionUs));
- loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false);
+ queue.removeAfter(periodHolder);
+ if (periodHolder.prepared) {
+ long loadingPeriodPositionUs =
+ Math.max(
+ periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs));
+ periodHolder.applyTrackSelection(loadingPeriodPositionUs, false);
+ updateLoadControlTrackSelection(periodHolder.trackSelectorResult);
}
}
- if (state != Player.STATE_ENDED) {
+ if (playbackInfo.playbackState != Player.STATE_ENDED) {
maybeContinueLoading();
updatePlaybackPositions();
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
- private boolean isTimelineReady(long playingPeriodDurationUs) {
+ private void updateLoadControlTrackSelection(TrackSelectorResult trackSelectorResult) {
+ loadControl.onTracksSelected(
+ renderers, trackSelectorResult.groups, trackSelectorResult.selections);
+ }
+
+ private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) {
+ MediaPeriodHolder periodHolder = queue.getFrontPeriod();
+ while (periodHolder != null) {
+ if (periodHolder.trackSelectorResult != null) {
+ TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll();
+ for (TrackSelection trackSelection : trackSelections) {
+ if (trackSelection != null) {
+ trackSelection.onPlaybackSpeed(playbackSpeed);
+ }
+ }
+ }
+ periodHolder = periodHolder.next;
+ }
+ }
+
+ private boolean shouldTransitionToReadyState(boolean renderersReadyOrEnded) {
+ if (enabledRenderers.length == 0) {
+ // If there are no enabled renderers, determine whether we're ready based on the timeline.
+ return isTimelineReady();
+ }
+ if (!renderersReadyOrEnded) {
+ return false;
+ }
+ if (!playbackInfo.isLoading) {
+ // Renderers are ready and we're not loading. Transition to ready, since the alternative is
+ // getting stuck waiting for additional media that's not being loaded.
+ return true;
+ }
+ // Renderers are ready and we're loading. Ask the LoadControl whether to transition.
+ MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
+ long bufferedPositionUs = loadingHolder.getBufferedPositionUs(!loadingHolder.info.isFinal);
+ return bufferedPositionUs == C.TIME_END_OF_SOURCE
+ || loadControl.shouldStartPlayback(
+ bufferedPositionUs - loadingHolder.toPeriodTime(rendererPositionUs),
+ mediaClock.getPlaybackParameters().speed,
+ rebuffering);
+ }
+
+ private boolean isTimelineReady() {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ long playingPeriodDurationUs = playingPeriodHolder.info.durationUs;
return playingPeriodDurationUs == C.TIME_UNSET
|| playbackInfo.positionUs < playingPeriodDurationUs
|| (playingPeriodHolder.next != null
- && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
+ && (playingPeriodHolder.next.prepared || playingPeriodHolder.next.info.id.isAd()));
}
private void maybeThrowPeriodPrepareError() throws IOException {
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared
&& (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) {
for (Renderer renderer : enabledRenderers) {
@@ -981,57 +1126,62 @@ import java.io.IOException;
Timeline oldTimeline = playbackInfo.timeline;
Timeline timeline = sourceRefreshInfo.timeline;
Object manifest = sourceRefreshInfo.manifest;
- mediaPeriodInfoSequence.setTimeline(timeline);
+ queue.setTimeline(timeline);
playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest);
+ resolvePendingMessagePositions();
if (oldTimeline == null) {
- int processedPrepareAcks = pendingPrepareCount;
+ playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount);
pendingPrepareCount = 0;
- if (pendingInitialSeekCount > 0) {
- Pair periodPosition = resolveSeekPosition(pendingSeekPosition);
- int processedInitialSeekCount = pendingInitialSeekCount;
- pendingInitialSeekCount = 0;
- pendingSeekPosition = null;
+ if (pendingInitialSeekPosition != null) {
+ Pair periodPosition =
+ resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true);
+ pendingInitialSeekPosition = null;
if (periodPosition == null) {
// The seek position was valid for the timeline that it was performed into, but the
// timeline has changed and a suitable seek position could not be resolved in the new one.
- handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, processedInitialSeekCount);
+ handleSourceInfoRefreshEndedPlayback();
} else {
int periodIndex = periodPosition.first;
long positionUs = periodPosition.second;
- MediaPeriodId periodId =
- mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs,
- positionUs);
- notifySourceInfoRefresh(processedPrepareAcks, processedInitialSeekCount);
+ MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, positionUs);
+ playbackInfo =
+ playbackInfo.fromNewPosition(
+ periodId, periodId.isAd() ? 0 : positionUs, /* contentPositionUs= */ positionUs);
}
} else if (playbackInfo.startPositionUs == C.TIME_UNSET) {
if (timeline.isEmpty()) {
- handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, 0);
+ handleSourceInfoRefreshEndedPlayback();
} else {
Pair defaultPosition = getPeriodPosition(timeline,
timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET);
int periodIndex = defaultPosition.first;
long startPositionUs = defaultPosition.second;
- MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex,
- startPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId,
- periodId.isAd() ? 0 : startPositionUs, startPositionUs);
- notifySourceInfoRefresh(processedPrepareAcks, 0);
+ MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, startPositionUs);
+ playbackInfo =
+ playbackInfo.fromNewPosition(
+ periodId,
+ periodId.isAd() ? 0 : startPositionUs,
+ /* contentPositionUs= */ startPositionUs);
}
- } else {
- notifySourceInfoRefresh(processedPrepareAcks, 0);
}
return;
}
int playingPeriodIndex = playbackInfo.periodId.periodIndex;
- MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder
- : loadingPeriodHolder;
- if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) {
- notifySourceInfoRefresh();
+ long contentPositionUs = playbackInfo.contentPositionUs;
+ if (oldTimeline.isEmpty()) {
+ // If the old timeline is empty, the period queue is also empty.
+ if (!timeline.isEmpty()) {
+ MediaPeriodId periodId =
+ queue.resolveMediaPeriodIdForAds(playingPeriodIndex, contentPositionUs);
+ playbackInfo =
+ playbackInfo.fromNewPosition(
+ periodId, periodId.isAd() ? 0 : contentPositionUs, contentPositionUs);
+ }
return;
}
+ MediaPeriodHolder periodHolder = queue.getFrontPeriod();
Object playingPeriodUid = periodHolder == null
? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid;
int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid);
@@ -1048,7 +1198,8 @@ import java.io.IOException;
Pair defaultPosition = getPeriodPosition(timeline,
timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET);
newPeriodIndex = defaultPosition.first;
- long newPositionUs = defaultPosition.second;
+ contentPositionUs = defaultPosition.second;
+ MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(newPeriodIndex, contentPositionUs);
timeline.getPeriod(newPeriodIndex, period, true);
if (periodHolder != null) {
// Clear the index of each holder that doesn't contain the default position. If a holder
@@ -1058,18 +1209,15 @@ import java.io.IOException;
while (periodHolder.next != null) {
periodHolder = periodHolder.next;
if (periodHolder.uid.equals(newPeriodUid)) {
- periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info,
- newPeriodIndex);
+ periodHolder.info = queue.getUpdatedMediaPeriodInfo(periodHolder.info, newPeriodIndex);
} else {
periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET);
}
}
}
// Actually do the seek.
- MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex);
- newPositionUs = seekToPeriodPosition(periodId, newPositionUs);
- playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET);
- notifySourceInfoRefresh();
+ long seekPositionUs = seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
+ playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
return;
}
@@ -1078,104 +1226,28 @@ import java.io.IOException;
playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex);
}
- if (playbackInfo.periodId.isAd()) {
- // Check that the playing ad hasn't been marked as played. If it has, skip forward.
- MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex,
- playbackInfo.contentPositionUs);
- if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) {
- long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs);
- long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET;
- playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs);
- notifySourceInfoRefresh();
+ MediaPeriodId playingPeriodId = playbackInfo.periodId;
+ if (playingPeriodId.isAd()) {
+ MediaPeriodId periodId = queue.resolveMediaPeriodIdForAds(periodIndex, contentPositionUs);
+ if (!periodId.equals(playingPeriodId)) {
+ // The previously playing ad should no longer be played, so skip it.
+ long seekPositionUs =
+ seekToPeriodPosition(periodId, periodId.isAd() ? 0 : contentPositionUs);
+ playbackInfo = playbackInfo.fromNewPosition(periodId, seekPositionUs, contentPositionUs);
return;
}
}
- if (periodHolder == null) {
- // We don't have any period holders, so we're done.
- notifySourceInfoRefresh();
- return;
- }
-
- // Update the holder indices. If we find a subsequent holder that's inconsistent with the new
- // timeline then take appropriate action.
- periodHolder = updatePeriodInfo(periodHolder, periodIndex);
- while (periodHolder.next != null) {
- MediaPeriodHolder previousPeriodHolder = periodHolder;
- periodHolder = periodHolder.next;
- periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode,
- shuffleModeEnabled);
- if (periodIndex != C.INDEX_UNSET
- && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
- // The holder is consistent with the new timeline. Update its index and continue.
- periodHolder = updatePeriodInfo(periodHolder, periodIndex);
- } else {
- // The holder is inconsistent with the new timeline.
- boolean seenReadingPeriodHolder =
- readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index;
- if (!seenReadingPeriodHolder) {
- // Renderers may have read from a period that's been removed. Seek back to the current
- // position of the playing period to make sure none of the removed period is played.
- long newPositionUs =
- seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs);
- playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, newPositionUs,
- playbackInfo.contentPositionUs);
- } else {
- // Update the loading period to be the last period that's still valid, and release all
- // subsequent periods.
- loadingPeriodHolder = previousPeriodHolder;
- loadingPeriodHolder.next = null;
- // Release the rest of the timeline.
- releasePeriodHoldersFrom(periodHolder);
- }
- break;
- }
- }
-
- notifySourceInfoRefresh();
- }
-
- private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) {
- while (true) {
- periodHolder.info =
- mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
- if (periodHolder.info.isLastInTimelinePeriod || periodHolder.next == null) {
- return periodHolder;
- }
- periodHolder = periodHolder.next;
+ if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) {
+ seekToCurrentPosition(/* sendDiscontinuity= */ false);
}
}
private void handleSourceInfoRefreshEndedPlayback() {
- handleSourceInfoRefreshEndedPlayback(0, 0);
- }
-
- private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) {
- Timeline timeline = playbackInfo.timeline;
- int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow(
- timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex;
- // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to
- // (firstPeriodIndex,0) isn't ignored.
- playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET);
setState(Player.STATE_ENDED);
- // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler.
- notifySourceInfoRefresh(prepareAcks, seekAcks,
- playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET));
// Reset, but retain the source so that it can still be used should a seek occur.
- resetInternal(false);
- }
-
- private void notifySourceInfoRefresh() {
- notifySourceInfoRefresh(0, 0);
- }
-
- private void notifySourceInfoRefresh(int prepareAcks, int seekAcks) {
- notifySourceInfoRefresh(prepareAcks, seekAcks, playbackInfo);
- }
-
- private void notifySourceInfoRefresh(int prepareAcks, int seekAcks, PlaybackInfo playbackInfo) {
- eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareAcks, seekAcks, playbackInfo)
- .sendToTarget();
+ resetInternal(
+ /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false);
}
/**
@@ -1188,8 +1260,8 @@ import java.io.IOException;
* @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET}
* if no such period was found.
*/
- private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline,
- Timeline newTimeline) {
+ private int resolveSubsequentPeriod(
+ int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) {
int newPeriodIndex = C.INDEX_UNSET;
int maxIterations = oldTimeline.getPeriodCount();
for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) {
@@ -1210,15 +1282,22 @@ import java.io.IOException;
* internal timeline.
*
* @param seekPosition The position to resolve.
+ * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching
+ * period if the original period is no longer available.
* @return The resolved position, or null if resolution was not successful.
* @throws IllegalSeekPositionException If the window index of the seek position is outside the
* bounds of the timeline.
*/
- private Pair resolveSeekPosition(SeekPosition seekPosition) {
+ private Pair resolveSeekPosition(
+ SeekPosition seekPosition, boolean trySubsequentPeriods) {
Timeline timeline = playbackInfo.timeline;
Timeline seekTimeline = seekPosition.timeline;
+ if (timeline == null) {
+ // We don't have a timeline yet, so we can't resolve the position.
+ return null;
+ }
if (seekTimeline.isEmpty()) {
- // The application performed a blind seek without a non-empty timeline (most likely based on
+ // The application performed a blind seek with an empty timeline (most likely based on
// knowledge of what the future timeline will be). Use the internal timeline.
seekTimeline = timeline;
}
@@ -1243,12 +1322,14 @@ import java.io.IOException;
// We successfully located the period in the internal timeline.
return Pair.create(periodIndex, periodPosition.second);
}
- // Try and find a subsequent period from the seek timeline in the internal timeline.
- periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
- if (periodIndex != C.INDEX_UNSET) {
- // We found one. Map the SeekPosition onto the corresponding default position.
- return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex,
- C.TIME_UNSET);
+ if (trySubsequentPeriods) {
+ // Try and find a subsequent period from the seek timeline in the internal timeline.
+ periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline);
+ if (periodIndex != C.INDEX_UNSET) {
+ // We found one. Map the SeekPosition onto the corresponding default position.
+ return getPeriodPosition(
+ timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET);
+ }
}
// We didn't find one. Give up.
return null;
@@ -1258,12 +1339,16 @@ import java.io.IOException;
* Calls {@link Timeline#getPeriodPosition(Timeline.Window, Timeline.Period, int, long)} using the
* current timeline.
*/
- private Pair getPeriodPosition(Timeline timeline, int windowIndex,
- long windowPositionUs) {
+ private Pair getPeriodPosition(
+ Timeline timeline, int windowIndex, long windowPositionUs) {
return timeline.getPeriodPosition(window, period, windowIndex, windowPositionUs);
}
private void updatePeriods() throws ExoPlaybackException, IOException {
+ if (mediaSource == null) {
+ // The player has no media source yet.
+ return;
+ }
if (playbackInfo.timeline == null) {
// We're waiting to get information about periods.
mediaSource.maybeThrowSourceInfoRefreshError();
@@ -1272,29 +1357,42 @@ import java.io.IOException;
// Update the loading period if required.
maybeUpdateLoadingPeriod();
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) {
setIsLoading(false);
- } else if (loadingPeriodHolder != null && !isLoading) {
+ } else if (!playbackInfo.isLoading) {
maybeContinueLoading();
}
- if (playingPeriodHolder == null) {
+ if (!queue.hasPlayingPeriod()) {
// We're waiting for the first period to be prepared.
return;
}
// Advance the playing period if necessary.
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
+ boolean advancedPlayingPeriod = false;
while (playWhenReady && playingPeriodHolder != readingPeriodHolder
&& rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) {
// All enabled renderers' streams have been read to the end, and the playback position reached
// the end of the playing period, so advance playback to the next period.
- playingPeriodHolder.release();
- setPlayingPeriodHolder(playingPeriodHolder.next);
+ if (advancedPlayingPeriod) {
+ // If we advance more than one period at a time, notify listeners after each update.
+ maybeNotifyPlaybackInfoChanged();
+ }
+ int discontinuityReason =
+ playingPeriodHolder.info.isLastInTimelinePeriod
+ ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
+ : Player.DISCONTINUITY_REASON_AD_INSERTION;
+ MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder;
+ playingPeriodHolder = queue.advancePlayingPeriod();
+ updatePlayingPeriodRenderers(oldPlayingPeriodHolder);
playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id,
playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs);
+ playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason);
updatePlaybackPositions();
- eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY,
- Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget();
+ advancedPlayingPeriod = true;
}
if (readingPeriodHolder.info.isFinal) {
@@ -1328,7 +1426,7 @@ import java.io.IOException;
}
TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
- readingPeriodHolder = readingPeriodHolder.next;
+ readingPeriodHolder = queue.advanceReadingPeriod();
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
boolean initialDiscontinuity =
@@ -1371,107 +1469,98 @@ import java.io.IOException;
}
private void maybeUpdateLoadingPeriod() throws IOException {
- MediaPeriodInfo info;
- if (loadingPeriodHolder == null) {
- info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo);
- } else {
- if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered()
- || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) {
- return;
+ queue.reevaluateBuffer(rendererPositionUs);
+ if (queue.shouldLoadNextMediaPeriod()) {
+ MediaPeriodInfo info = queue.getNextMediaPeriodInfo(rendererPositionUs, playbackInfo);
+ if (info == null) {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ } else {
+ Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid;
+ MediaPeriod mediaPeriod =
+ queue.enqueueNextMediaPeriod(
+ rendererCapabilities,
+ RENDERER_TIMESTAMP_OFFSET_US,
+ trackSelector,
+ loadControl.getAllocator(),
+ mediaSource,
+ uid,
+ info);
+ mediaPeriod.prepare(this, info.startPositionUs);
+ setIsLoading(true);
}
- if (playingPeriodHolder != null) {
- int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index;
- if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) {
- // We are already buffering the maximum number of periods ahead.
- return;
- }
- }
- info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info,
- loadingPeriodHolder.getRendererOffset(), rendererPositionUs);
}
- if (info == null) {
- mediaSource.maybeThrowSourceInfoRefreshError();
- return;
- }
-
- long rendererPositionOffsetUs = loadingPeriodHolder == null
- ? RENDERER_TIMESTAMP_OFFSET_US
- : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs);
- int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1;
- Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid;
- MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities,
- rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info);
- if (loadingPeriodHolder != null) {
- loadingPeriodHolder.next = newPeriodHolder;
- }
- loadingPeriodHolder = newPeriodHolder;
- loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs);
- setIsLoading(true);
}
- private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException {
- if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+ private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
+ if (!queue.isLoading(mediaPeriod)) {
// Stale event.
return;
}
- loadingPeriodHolder.handlePrepared();
- if (playingPeriodHolder == null) {
+ TrackSelectorResult trackSelectorResult =
+ queue.handleLoadingPeriodPrepared(mediaClock.getPlaybackParameters().speed);
+ updateLoadControlTrackSelection(trackSelectorResult);
+ if (!queue.hasPlayingPeriod()) {
// This is the first prepared period, so start playing it.
- readingPeriodHolder = loadingPeriodHolder;
- resetRendererPosition(readingPeriodHolder.info.startPositionUs);
- setPlayingPeriodHolder(readingPeriodHolder);
+ MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod();
+ resetRendererPosition(playingPeriodHolder.info.startPositionUs);
+ updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null);
}
maybeContinueLoading();
}
- private void handleContinueLoadingRequested(MediaPeriod period) {
- if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) {
+ private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
+ if (!queue.isLoading(mediaPeriod)) {
// Stale event.
return;
}
+ queue.reevaluateBuffer(rendererPositionUs);
maybeContinueLoading();
}
private void maybeContinueLoading() {
- boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs);
+ MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod();
+ long nextLoadPositionUs = loadingPeriodHolder.getNextLoadPositionUs();
+ if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
+ setIsLoading(false);
+ return;
+ }
+ long bufferedDurationUs =
+ nextLoadPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs);
+ boolean continueLoading =
+ loadControl.shouldContinueLoading(
+ bufferedDurationUs, mediaClock.getPlaybackParameters().speed);
setIsLoading(continueLoading);
if (continueLoading) {
loadingPeriodHolder.continueLoading(rendererPositionUs);
}
}
- private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) {
- while (periodHolder != null) {
- periodHolder.release();
- periodHolder = periodHolder.next;
- }
- }
-
- private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException {
- if (playingPeriodHolder == periodHolder) {
+ private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder)
+ throws ExoPlaybackException {
+ MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod();
+ if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) {
return;
}
-
int enabledRendererCount = 0;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
- if (periodHolder.trackSelectorResult.renderersEnabled[i]) {
+ if (newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]) {
enabledRendererCount++;
}
- if (rendererWasEnabledFlags[i] && (!periodHolder.trackSelectorResult.renderersEnabled[i]
- || (renderer.isCurrentStreamFinal()
- && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
+ if (rendererWasEnabledFlags[i]
+ && (!newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]
+ || (renderer.isCurrentStreamFinal()
+ && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) {
// The renderer should be disabled before playing the next period, either because it's not
// needed to play the next period, or because we need to re-enable it as its current stream
// is final and it's not reading ahead.
disableRenderer(renderer);
}
}
-
- playingPeriodHolder = periodHolder;
- eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
+ playbackInfo =
+ playbackInfo.copyWithTrackSelectorResult(newPlayingPeriodHolder.trackSelectorResult);
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
}
@@ -1479,6 +1568,7 @@ import java.io.IOException;
throws ExoPlaybackException {
enabledRenderers = new Renderer[totalEnabledRendererCount];
int enabledRendererCount = 0;
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
for (int i = 0; i < renderers.length; i++) {
if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) {
enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++);
@@ -1486,8 +1576,10 @@ import java.io.IOException;
}
}
- private void enableRenderer(int rendererIndex, boolean wasRendererEnabled,
- int enabledRendererIndex) throws ExoPlaybackException {
+ private void enableRenderer(
+ int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex)
+ throws ExoPlaybackException {
+ MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
Renderer renderer = renderers[rendererIndex];
enabledRenderers[enabledRendererIndex] = renderer;
if (renderer.getState() == Renderer.STATE_DISABLED) {
@@ -1497,23 +1589,14 @@ import java.io.IOException;
rendererIndex);
Format[] formats = getFormats(newSelection);
// The renderer needs enabling with its new track selection.
- boolean playing = playWhenReady && state == Player.STATE_READY;
+ boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY;
// Consider as joining only if the renderer was previously disabled.
boolean joining = !wasRendererEnabled && playing;
// Enable the renderer.
renderer.enable(rendererConfiguration, formats,
playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs,
joining, playingPeriodHolder.getRendererOffset());
- MediaClock mediaClock = renderer.getMediaClock();
- if (mediaClock != null) {
- if (rendererMediaClock != null) {
- throw ExoPlaybackException.createForUnexpected(
- new IllegalStateException("Multiple renderer media clocks enabled."));
- }
- rendererMediaClock = mediaClock;
- rendererMediaClockSource = renderer;
- rendererMediaClock.setPlaybackParameters(playbackParameters);
- }
+ mediaClock.onRendererEnabled(renderer);
// Start the renderer if playing.
if (playing) {
renderer.start();
@@ -1522,6 +1605,7 @@ import java.io.IOException;
}
private boolean rendererWaitingForNextStream(Renderer renderer) {
+ MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
return readingPeriodHolder.next != null && readingPeriodHolder.next.prepared
&& renderer.hasReadStreamToEnd();
}
@@ -1537,231 +1621,6 @@ import java.io.IOException;
return formats;
}
- /**
- * Holds a {@link MediaPeriod} with information required to play it as part of a timeline.
- */
- private static final class MediaPeriodHolder {
-
- public final MediaPeriod mediaPeriod;
- public final Object uid;
- public final int index;
- public final SampleStream[] sampleStreams;
- public final boolean[] mayRetainStreamFlags;
- public final long rendererPositionOffsetUs;
-
- public MediaPeriodInfo info;
- public boolean prepared;
- public boolean hasEnabledTracks;
- public MediaPeriodHolder next;
- public TrackSelectorResult trackSelectorResult;
-
- private final Renderer[] renderers;
- private final RendererCapabilities[] rendererCapabilities;
- private final TrackSelector trackSelector;
- private final LoadControl loadControl;
- private final MediaSource mediaSource;
-
- private TrackSelectorResult periodTrackSelectorResult;
-
- public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
- long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
- MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) {
- this.renderers = renderers;
- this.rendererCapabilities = rendererCapabilities;
- this.rendererPositionOffsetUs = rendererPositionOffsetUs;
- this.trackSelector = trackSelector;
- this.loadControl = loadControl;
- this.mediaSource = mediaSource;
- this.uid = Assertions.checkNotNull(periodUid);
- this.index = index;
- this.info = info;
- sampleStreams = new SampleStream[renderers.length];
- mayRetainStreamFlags = new boolean[renderers.length];
- MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, loadControl.getAllocator());
- if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
- ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true);
- clippingMediaPeriod.setClipping(0, info.endPositionUs);
- mediaPeriod = clippingMediaPeriod;
- }
- this.mediaPeriod = mediaPeriod;
- }
-
- public long toRendererTime(long periodTimeUs) {
- return periodTimeUs + getRendererOffset();
- }
-
- public long toPeriodTime(long rendererTimeUs) {
- return rendererTimeUs - getRendererOffset();
- }
-
- public long getRendererOffset() {
- return index == 0 ? rendererPositionOffsetUs
- : (rendererPositionOffsetUs - info.startPositionUs);
- }
-
- public boolean isFullyBuffered() {
- return prepared
- && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
- }
-
- public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) {
- long bufferedPositionUs = !prepared ? info.startPositionUs
- : mediaPeriod.getBufferedPositionUs();
- if (bufferedPositionUs == C.TIME_END_OF_SOURCE) {
- if (info.isFinal) {
- return true;
- }
- bufferedPositionUs = info.durationUs;
- }
- return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs),
- rebuffering);
- }
-
- public void handlePrepared() throws ExoPlaybackException {
- prepared = true;
- selectTracks();
- long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false);
- info = info.copyWithStartPositionUs(newStartPositionUs);
- }
-
- public boolean shouldContinueLoading(long rendererPositionUs) {
- long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
- if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) {
- return false;
- } else {
- long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
- long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs;
- return loadControl.shouldContinueLoading(bufferedDurationUs);
- }
- }
-
- public void continueLoading(long rendererPositionUs) {
- long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
- mediaPeriod.continueLoading(loadingPeriodPositionUs);
- }
-
- public boolean selectTracks() throws ExoPlaybackException {
- TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
- mediaPeriod.getTrackGroups());
- if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
- return false;
- }
- trackSelectorResult = selectorResult;
- return true;
- }
-
- public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) {
- return updatePeriodTrackSelection(positionUs, forceRecreateStreams,
- new boolean[renderers.length]);
- }
-
- public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
- boolean[] streamResetFlags) {
- TrackSelectionArray trackSelections = trackSelectorResult.selections;
- for (int i = 0; i < trackSelections.length; i++) {
- mayRetainStreamFlags[i] = !forceRecreateStreams
- && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
- }
-
- // Undo the effect of previous call to associate no-sample renderers with empty tracks
- // so the mediaPeriod receives back whatever it sent us before.
- disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
- updatePeriodTrackSelectorResult(trackSelectorResult);
- // Disable streams on the period and get new streams for updated/newly-enabled tracks.
- positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
- sampleStreams, streamResetFlags, positionUs);
- associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
-
- // Update whether we have enabled tracks and sanity check the expected streams are non-null.
- hasEnabledTracks = false;
- for (int i = 0; i < sampleStreams.length; i++) {
- if (sampleStreams[i] != null) {
- Assertions.checkState(trackSelectorResult.renderersEnabled[i]);
- // hasEnabledTracks should be true only when non-empty streams exists.
- if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) {
- hasEnabledTracks = true;
- }
- } else {
- Assertions.checkState(trackSelections.get(i) == null);
- }
- }
-
- // The track selection has changed.
- loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
- return positionUs;
- }
-
- public void release() {
- updatePeriodTrackSelectorResult(null);
- try {
- if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
- mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
- } else {
- mediaSource.releasePeriod(mediaPeriod);
- }
- } catch (RuntimeException e) {
- // There's nothing we can do.
- Log.e(TAG, "Period release failed.", e);
- }
- }
-
- private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
- if (periodTrackSelectorResult != null) {
- disableTrackSelectionsInResult(periodTrackSelectorResult);
- }
- periodTrackSelectorResult = trackSelectorResult;
- if (periodTrackSelectorResult != null) {
- enableTrackSelectionsInResult(periodTrackSelectorResult);
- }
- }
-
- private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
- for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
- boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
- TrackSelection trackSelection = trackSelectorResult.selections.get(i);
- if (rendererEnabled && trackSelection != null) {
- trackSelection.enable();
- }
- }
- }
-
- private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
- for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
- boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
- TrackSelection trackSelection = trackSelectorResult.selections.get(i);
- if (rendererEnabled && trackSelection != null) {
- trackSelection.disable();
- }
- }
- }
-
- /**
- * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy
- * {@link EmptySampleStream} that was associated with it.
- */
- private void disassociateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
- for (int i = 0; i < rendererCapabilities.length; i++) {
- if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) {
- sampleStreams[i] = null;
- }
- }
- }
-
- /**
- * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will
- * associate it with a dummy {@link EmptySampleStream}.
- */
- private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
- for (int i = 0; i < rendererCapabilities.length; i++) {
- if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE
- && trackSelectorResult.renderersEnabled[i]) {
- sampleStreams[i] = new EmptySampleStream();
- }
- }
- }
-
- }
-
private static final class SeekPosition {
public final Timeline timeline;
@@ -1773,7 +1632,43 @@ import java.io.IOException;
this.windowIndex = windowIndex;
this.windowPositionUs = windowPositionUs;
}
+ }
+ private static final class PendingMessageInfo implements Comparable {
+
+ public final PlayerMessage message;
+
+ public int resolvedPeriodIndex;
+ public long resolvedPeriodTimeUs;
+ public @Nullable Object resolvedPeriodUid;
+
+ public PendingMessageInfo(PlayerMessage message) {
+ this.message = message;
+ }
+
+ public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) {
+ resolvedPeriodIndex = periodIndex;
+ resolvedPeriodTimeUs = periodTimeUs;
+ resolvedPeriodUid = periodUid;
+ }
+
+ @Override
+ public int compareTo(@NonNull PendingMessageInfo other) {
+ if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) {
+ // PendingMessageInfos with a resolved period position are always smaller.
+ return resolvedPeriodUid != null ? -1 : 1;
+ }
+ if (resolvedPeriodUid == null) {
+ // Don't sort message with unresolved positions.
+ return 0;
+ }
+ // Sort resolved media times by period index and then by period position.
+ int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex;
+ if (comparePeriodIndex != 0) {
+ return comparePeriodIndex;
+ }
+ return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs);
+ }
}
private static final class MediaSourceRefreshInfo {
@@ -1787,7 +1682,40 @@ import java.io.IOException;
this.timeline = timeline;
this.manifest = manifest;
}
+ }
+ private static final class PlaybackInfoUpdate {
+
+ private PlaybackInfo lastPlaybackInfo;
+ private int operationAcks;
+ private boolean positionDiscontinuity;
+ private @DiscontinuityReason int discontinuityReason;
+
+ public boolean hasPendingUpdate(PlaybackInfo playbackInfo) {
+ return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity;
+ }
+
+ public void reset(PlaybackInfo playbackInfo) {
+ lastPlaybackInfo = playbackInfo;
+ operationAcks = 0;
+ positionDiscontinuity = false;
+ }
+
+ public void incrementPendingOperationAcks(int operationAcks) {
+ this.operationAcks += operationAcks;
+ }
+
+ public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) {
+ if (positionDiscontinuity
+ && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) {
+ // We always prefer non-internal discontinuity reasons. We also assume that we won't report
+ // more than one non-internal discontinuity per message iteration.
+ Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL);
+ return;
+ }
+ positionDiscontinuity = true;
+ this.discontinuityReason = discontinuityReason;
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
index b2200b6671..1dec506ec9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java
@@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo {
* The version of the library expressed as a string, for example "1.2.3".
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
- public static final String VERSION = "2.6.1";
+ public static final String VERSION = "2.7.0";
/**
* The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}.
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1";
+ public static final String VERSION_SLASHY = "ExoPlayerLib/2.7.0";
/**
* The version of the library expressed as an integer, for example 1002003.
@@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
- public static final int VERSION_INT = 2006001;
+ public static final int VERSION_INT = 2007000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
index 4bd23e2cb6..c830a246ae 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java
@@ -21,7 +21,6 @@ import android.media.MediaFormat;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.drm.DrmInitData;
-import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@@ -324,11 +323,41 @@ public final class Format implements Parcelable {
// Image.
- public static Format createImageSampleFormat(String id, String sampleMimeType, String codecs,
- int bitrate, List initializationData, String language, DrmInitData drmInitData) {
- return new Format(id, null, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
- NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData,
+ public static Format createImageSampleFormat(
+ String id,
+ String sampleMimeType,
+ String codecs,
+ int bitrate,
+ @C.SelectionFlags int selectionFlags,
+ List initializationData,
+ String language,
+ DrmInitData drmInitData) {
+ return new Format(
+ id,
+ null,
+ sampleMimeType,
+ codecs,
+ bitrate,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ null,
+ NO_VALUE,
+ null,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ NO_VALUE,
+ selectionFlags,
+ language,
+ NO_VALUE,
+ OFFSET_SAMPLE_RELATIVE,
+ initializationData,
+ drmInitData,
null);
}
@@ -444,8 +473,15 @@ public final class Format implements Parcelable {
drmInitData, metadata);
}
- public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
- @C.SelectionFlags int selectionFlags, String language) {
+ public Format copyWithContainerInfo(
+ String id,
+ String sampleMimeType,
+ String codecs,
+ int bitrate,
+ int width,
+ int height,
+ @C.SelectionFlags int selectionFlags,
+ String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
@@ -465,8 +501,8 @@ public final class Format implements Parcelable {
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
String language = this.language == null ? manifestFormat.language : this.language;
- DrmInitData drmInitData = manifestFormat.drmInitData != null
- ? getFilledManifestDrmData(manifestFormat.drmInitData) : this.drmInitData;
+ DrmInitData drmInitData =
+ DrmInitData.createSessionCreationData(manifestFormat.drmInitData, this.drmInitData);
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
@@ -731,43 +767,4 @@ public final class Format implements Parcelable {
}
};
-
- private DrmInitData getFilledManifestDrmData(DrmInitData manifestDrmData) {
- // All exposed SchemeDatas must include key request information.
- ArrayList exposedSchemeDatas = new ArrayList<>();
- ArrayList emptySchemeDatas = new ArrayList<>();
- for (int i = 0; i < manifestDrmData.schemeDataCount; i++) {
- SchemeData schemeData = manifestDrmData.get(i);
- if (schemeData.hasData()) {
- exposedSchemeDatas.add(schemeData);
- } else /* needs initialization data filling */ {
- emptySchemeDatas.add(schemeData);
- }
- }
-
- if (emptySchemeDatas.isEmpty()) {
- // Manifest DRM information is complete.
- return manifestDrmData;
- } else if (drmInitData == null) {
- // The manifest DRM data needs filling but this format does not include enough information to
- // do it. A subset of the manifest's scheme datas should not be exposed because a
- // DrmSessionManager could decide it does not support the format, while the missing
- // information comes in a format feed immediately after.
- return null;
- }
-
- int needFillingCount = emptySchemeDatas.size();
- for (int i = 0; i < drmInitData.schemeDataCount; i++) {
- SchemeData mediaSchemeData = drmInitData.get(i);
- for (int j = 0; j < needFillingCount; j++) {
- if (mediaSchemeData.canReplace(emptySchemeDatas.get(j))) {
- exposedSchemeDatas.add(mediaSchemeData);
- break;
- }
- }
- }
- return exposedSchemeDatas.isEmpty() ? null : new DrmInitData(manifestDrmData.schemeType,
- exposedSchemeDatas.toArray(new SchemeData[exposedSchemeDatas.size()]));
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
index c092480222..80be0b9e71 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java
@@ -56,23 +56,58 @@ public interface LoadControl {
Allocator getAllocator();
/**
- * Called by the player to determine whether sufficient media is buffered for playback to be
- * started or resumed.
+ * Returns the duration of media to retain in the buffer prior to the current playback position,
+ * for fast backward seeking.
+ *
+ * Note: If {@link #retainBackBufferFromKeyframe()} is false then seeking in the back-buffer will
+ * only be fast if the back-buffer contains a keyframe prior to the seek position.
+ *
+ * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not
+ * currently supported.
*
- * @param bufferedDurationUs The duration of media that's currently buffered.
- * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
- * buffer depletion rather than a user action. Hence this parameter is false during initial
- * buffering and when buffering as a result of a seek operation.
- * @return Whether playback should be allowed to start or resume.
+ * @return The duration of media to retain in the buffer prior to the current playback position,
+ * in microseconds.
*/
- boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering);
+ long getBackBufferDurationUs();
+
+ /**
+ * Returns whether media should be retained from the keyframe before the current playback position
+ * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.
+ *
+ * Warning: Returning true will cause the back-buffer size to depend on the spacing of keyframes
+ * in the media being played. Returning true is not recommended unless you control the media and
+ * are comfortable with the back-buffer size exceeding {@link #getBackBufferDurationUs()} by as
+ * much as the maximum duration between adjacent keyframes in the media.
+ *
+ * Note: Implementations should return a single value. Dynamic changes to the back-buffer are not
+ * currently supported.
+ *
+ * @return Whether media should be retained from the keyframe before the current playback position
+ * minus {@link #getBackBufferDurationUs()}, rather than any sample before or at that position.
+ */
+ boolean retainBackBufferFromKeyframe();
/**
* Called by the player to determine whether it should continue to load the source.
*
* @param bufferedDurationUs The duration of media that's currently buffered.
+ * @param playbackSpeed The current playback speed.
* @return Whether the loading should continue.
*/
- boolean shouldContinueLoading(long bufferedDurationUs);
+ boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed);
+ /**
+ * Called repeatedly by the player when it's loading the source, has yet to start playback, and
+ * has the minimum amount of data necessary for playback to be started. The value returned
+ * determines whether playback is actually started. The load control may opt to return {@code
+ * false} until some condition has been met (e.g. a certain amount of media is buffered).
+ *
+ * @param bufferedDurationUs The duration of media that's currently buffered.
+ * @param playbackSpeed The current playback speed.
+ * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by
+ * buffer depletion rather than a user action. Hence this parameter is false during initial
+ * buffering and when buffering as a result of a seek operation.
+ * @return Whether playback should be allowed to start or resume.
+ */
+ boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
new file mode 100644
index 0000000000..43036b154b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.util.Log;
+import com.google.android.exoplayer2.source.ClippingMediaPeriod;
+import com.google.android.exoplayer2.source.EmptySampleStream;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+
+/** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */
+/* package */ final class MediaPeriodHolder {
+
+ private static final String TAG = "MediaPeriodHolder";
+
+ public final MediaPeriod mediaPeriod;
+ public final Object uid;
+ public final SampleStream[] sampleStreams;
+ public final boolean[] mayRetainStreamFlags;
+
+ public long rendererPositionOffsetUs;
+ public boolean prepared;
+ public boolean hasEnabledTracks;
+ public MediaPeriodInfo info;
+ public MediaPeriodHolder next;
+ public TrackSelectorResult trackSelectorResult;
+
+ private final RendererCapabilities[] rendererCapabilities;
+ private final TrackSelector trackSelector;
+ private final MediaSource mediaSource;
+
+ private TrackSelectorResult periodTrackSelectorResult;
+
+ /**
+ * Creates a new holder with information required to play it as part of a timeline.
+ *
+ * @param rendererCapabilities The renderer capabilities.
+ * @param rendererPositionOffsetUs The time offset of the start of the media period to provide to
+ * renderers.
+ * @param trackSelector The track selector.
+ * @param allocator The allocator.
+ * @param mediaSource The media source that produced the media period.
+ * @param uid The unique identifier for the containing timeline period.
+ * @param info Information used to identify this media period in its timeline period.
+ */
+ public MediaPeriodHolder(
+ RendererCapabilities[] rendererCapabilities,
+ long rendererPositionOffsetUs,
+ TrackSelector trackSelector,
+ Allocator allocator,
+ MediaSource mediaSource,
+ Object uid,
+ MediaPeriodInfo info) {
+ this.rendererCapabilities = rendererCapabilities;
+ this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs;
+ this.trackSelector = trackSelector;
+ this.mediaSource = mediaSource;
+ this.uid = Assertions.checkNotNull(uid);
+ this.info = info;
+ sampleStreams = new SampleStream[rendererCapabilities.length];
+ mayRetainStreamFlags = new boolean[rendererCapabilities.length];
+ MediaPeriod mediaPeriod = mediaSource.createPeriod(info.id, allocator);
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ ClippingMediaPeriod clippingMediaPeriod = new ClippingMediaPeriod(mediaPeriod, true);
+ clippingMediaPeriod.setClipping(0, info.endPositionUs);
+ mediaPeriod = clippingMediaPeriod;
+ }
+ this.mediaPeriod = mediaPeriod;
+ }
+
+ public long toRendererTime(long periodTimeUs) {
+ return periodTimeUs + getRendererOffset();
+ }
+
+ public long toPeriodTime(long rendererTimeUs) {
+ return rendererTimeUs - getRendererOffset();
+ }
+
+ public long getRendererOffset() {
+ return rendererPositionOffsetUs;
+ }
+
+ public boolean isFullyBuffered() {
+ return prepared
+ && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE);
+ }
+
+ public long getDurationUs() {
+ return info.durationUs;
+ }
+
+ /**
+ * Returns the buffered position in microseconds. If the period is buffered to the end then
+ * {@link C#TIME_END_OF_SOURCE} is returned unless {@code convertEosToDuration} is true, in which
+ * case the period duration is returned.
+ *
+ * @param convertEosToDuration Whether to return the period duration rather than
+ * {@link C#TIME_END_OF_SOURCE} if the period is fully buffered.
+ * @return The buffered position in microseconds.
+ */
+ public long getBufferedPositionUs(boolean convertEosToDuration) {
+ if (!prepared) {
+ return info.startPositionUs;
+ }
+ long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
+ return bufferedPositionUs == C.TIME_END_OF_SOURCE && convertEosToDuration
+ ? info.durationUs
+ : bufferedPositionUs;
+ }
+
+ public long getNextLoadPositionUs() {
+ return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs();
+ }
+
+ public TrackSelectorResult handlePrepared(float playbackSpeed) throws ExoPlaybackException {
+ prepared = true;
+ selectTracks(playbackSpeed);
+ long newStartPositionUs = applyTrackSelection(info.startPositionUs, false);
+ rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs;
+ info = info.copyWithStartPositionUs(newStartPositionUs);
+ return trackSelectorResult;
+ }
+
+ public void reevaluateBuffer(long rendererPositionUs) {
+ if (prepared) {
+ mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs));
+ }
+ }
+
+ public void continueLoading(long rendererPositionUs) {
+ long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs);
+ mediaPeriod.continueLoading(loadingPeriodPositionUs);
+ }
+
+ public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException {
+ TrackSelectorResult selectorResult =
+ trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups());
+ if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
+ return false;
+ }
+ trackSelectorResult = selectorResult;
+ for (TrackSelection trackSelection : trackSelectorResult.selections.getAll()) {
+ if (trackSelection != null) {
+ trackSelection.onPlaybackSpeed(playbackSpeed);
+ }
+ }
+ return true;
+ }
+
+ public long applyTrackSelection(long positionUs, boolean forceRecreateStreams) {
+ return applyTrackSelection(
+ positionUs, forceRecreateStreams, new boolean[rendererCapabilities.length]);
+ }
+
+ public long applyTrackSelection(
+ long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) {
+ TrackSelectionArray trackSelections = trackSelectorResult.selections;
+ for (int i = 0; i < trackSelections.length; i++) {
+ mayRetainStreamFlags[i] =
+ !forceRecreateStreams && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
+ }
+
+ // Undo the effect of previous call to associate no-sample renderers with empty tracks
+ // so the mediaPeriod receives back whatever it sent us before.
+ disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams);
+ updatePeriodTrackSelectorResult(trackSelectorResult);
+ // Disable streams on the period and get new streams for updated/newly-enabled tracks.
+ positionUs =
+ mediaPeriod.selectTracks(
+ trackSelections.getAll(),
+ mayRetainStreamFlags,
+ sampleStreams,
+ streamResetFlags,
+ positionUs);
+ associateNoSampleRenderersWithEmptySampleStream(sampleStreams);
+
+ // Update whether we have enabled tracks and sanity check the expected streams are non-null.
+ hasEnabledTracks = false;
+ for (int i = 0; i < sampleStreams.length; i++) {
+ if (sampleStreams[i] != null) {
+ Assertions.checkState(trackSelectorResult.renderersEnabled[i]);
+ // hasEnabledTracks should be true only when non-empty streams exists.
+ if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) {
+ hasEnabledTracks = true;
+ }
+ } else {
+ Assertions.checkState(trackSelections.get(i) == null);
+ }
+ }
+ return positionUs;
+ }
+
+ public void release() {
+ updatePeriodTrackSelectorResult(null);
+ try {
+ if (info.endPositionUs != C.TIME_END_OF_SOURCE) {
+ mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
+ } else {
+ mediaSource.releasePeriod(mediaPeriod);
+ }
+ } catch (RuntimeException e) {
+ // There's nothing we can do.
+ Log.e(TAG, "Period release failed.", e);
+ }
+ }
+
+ private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
+ if (periodTrackSelectorResult != null) {
+ disableTrackSelectionsInResult(periodTrackSelectorResult);
+ }
+ periodTrackSelectorResult = trackSelectorResult;
+ if (periodTrackSelectorResult != null) {
+ enableTrackSelectionsInResult(periodTrackSelectorResult);
+ }
+ }
+
+ private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
+ for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
+ boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
+ TrackSelection trackSelection = trackSelectorResult.selections.get(i);
+ if (rendererEnabled && trackSelection != null) {
+ trackSelection.enable();
+ }
+ }
+ }
+
+ private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) {
+ for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) {
+ boolean rendererEnabled = trackSelectorResult.renderersEnabled[i];
+ TrackSelection trackSelection = trackSelectorResult.selections.get(i);
+ if (rendererEnabled && trackSelection != null) {
+ trackSelection.disable();
+ }
+ }
+ }
+
+ /**
+ * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@link
+ * EmptySampleStream} that was associated with it.
+ */
+ private void disassociateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) {
+ sampleStreams[i] = null;
+ }
+ }
+ }
+
+ /**
+ * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with
+ * a dummy {@link EmptySampleStream}.
+ */
+ private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) {
+ for (int i = 0; i < rendererCapabilities.length; i++) {
+ if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE
+ && trackSelectorResult.renderersEnabled[i]) {
+ sampleStreams[i] = new EmptySampleStream();
+ }
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
new file mode 100644
index 0000000000..fce1780b71
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2018 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;
+
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+
+/** Stores the information required to load and play a {@link MediaPeriod}. */
+/* package */ final class MediaPeriodInfo {
+
+ /** The media period's identifier. */
+ public final MediaPeriodId id;
+ /** The start position of the media to play within the media period, in microseconds. */
+ public final long startPositionUs;
+ /**
+ * The end position of the media to play within the media period, in microseconds, or {@link
+ * C#TIME_END_OF_SOURCE} if the end position is the end of the media period.
+ */
+ public final long endPositionUs;
+ /**
+ * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
+ * otherwise.
+ */
+ public final long contentPositionUs;
+ /**
+ * The duration of the media period, like {@link #endPositionUs} but with {@link
+ * C#TIME_END_OF_SOURCE} resolved to the timeline period duration. May be {@link C#TIME_UNSET} if
+ * the end position is not known.
+ */
+ public final long durationUs;
+ /**
+ * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media
+ * period corresponding to a timeline period without ads).
+ */
+ public final boolean isLastInTimelinePeriod;
+ /**
+ * Whether this is the last media period in the entire timeline. If true, {@link
+ * #isLastInTimelinePeriod} will also be true.
+ */
+ public final boolean isFinal;
+
+ MediaPeriodInfo(
+ MediaPeriodId id,
+ long startPositionUs,
+ long endPositionUs,
+ long contentPositionUs,
+ long durationUs,
+ boolean isLastInTimelinePeriod,
+ boolean isFinal) {
+ this.id = id;
+ this.startPositionUs = startPositionUs;
+ this.endPositionUs = endPositionUs;
+ this.contentPositionUs = contentPositionUs;
+ this.durationUs = durationUs;
+ this.isLastInTimelinePeriod = isLastInTimelinePeriod;
+ this.isFinal = isFinal;
+ }
+
+ /**
+ * Returns a copy of this instance with the period identifier's period index set to the specified
+ * value.
+ */
+ public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) {
+ return new MediaPeriodInfo(
+ id.copyWithPeriodIndex(periodIndex),
+ startPositionUs,
+ endPositionUs,
+ contentPositionUs,
+ durationUs,
+ isLastInTimelinePeriod,
+ isFinal);
+ }
+
+ /** Returns a copy of this instance with the start position set to the specified value. */
+ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
+ return new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ endPositionUs,
+ contentPositionUs,
+ durationUs,
+ isLastInTimelinePeriod,
+ isFinal);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
deleted file mode 100644
index 6cb76e5471..0000000000
--- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java
+++ /dev/null
@@ -1,359 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.google.android.exoplayer2;
-
-import android.util.Pair;
-import com.google.android.exoplayer2.Player.RepeatMode;
-import com.google.android.exoplayer2.source.MediaPeriod;
-import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
-
-/**
- * Provides a sequence of {@link MediaPeriodInfo}s to the player, determining the order and
- * start/end positions for {@link MediaPeriod}s to load and play.
- */
-/* package */ final class MediaPeriodInfoSequence {
-
- // TODO: Consider merging this class with the MediaPeriodHolder queue in ExoPlayerImplInternal.
-
- /**
- * Stores the information required to load and play a {@link MediaPeriod}.
- */
- public static final class MediaPeriodInfo {
-
- /**
- * The media period's identifier.
- */
- public final MediaPeriodId id;
- /**
- * The start position of the media to play within the media period, in microseconds.
- */
- public final long startPositionUs;
- /**
- * The end position of the media to play within the media period, in microseconds, or
- * {@link C#TIME_END_OF_SOURCE} if the end position is the end of the media period.
- */
- public final long endPositionUs;
- /**
- * If this is an ad, the position to play in the next content media period. {@link C#TIME_UNSET}
- * otherwise.
- */
- public final long contentPositionUs;
- /**
- * The duration of the media to play within the media period, in microseconds, or
- * {@link C#TIME_UNSET} if not known.
- */
- public final long durationUs;
- /**
- * Whether this is the last media period in its timeline period (e.g., a postroll ad, or a media
- * period corresponding to a timeline period without ads).
- */
- public final boolean isLastInTimelinePeriod;
- /**
- * Whether this is the last media period in the entire timeline. If true,
- * {@link #isLastInTimelinePeriod} will also be true.
- */
- public final boolean isFinal;
-
- private MediaPeriodInfo(MediaPeriodId id, long startPositionUs, long endPositionUs,
- long contentPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) {
- this.id = id;
- this.startPositionUs = startPositionUs;
- this.endPositionUs = endPositionUs;
- this.contentPositionUs = contentPositionUs;
- this.durationUs = durationUs;
- this.isLastInTimelinePeriod = isLastInTimelinePeriod;
- this.isFinal = isFinal;
- }
-
- /**
- * Returns a copy of this instance with the period identifier's period index set to the
- * specified value.
- */
- public MediaPeriodInfo copyWithPeriodIndex(int periodIndex) {
- return new MediaPeriodInfo(id.copyWithPeriodIndex(periodIndex), startPositionUs,
- endPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal);
- }
-
- /**
- * Returns a copy of this instance with the start position set to the specified value.
- */
- public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) {
- return new MediaPeriodInfo(id, startPositionUs, endPositionUs, contentPositionUs, durationUs,
- isLastInTimelinePeriod, isFinal);
- }
-
- }
-
- private final Timeline.Period period;
- private final Timeline.Window window;
-
- private Timeline timeline;
- private @RepeatMode int repeatMode;
- private boolean shuffleModeEnabled;
-
- /**
- * Creates a new media period info sequence.
- */
- public MediaPeriodInfoSequence() {
- period = new Timeline.Period();
- window = new Timeline.Window();
- }
-
- /**
- * Sets the {@link Timeline}. Call {@link #getUpdatedMediaPeriodInfo} to update period information
- * taking into account the new timeline.
- */
- public void setTimeline(Timeline timeline) {
- this.timeline = timeline;
- }
-
- /**
- * Sets the {@link RepeatMode}. Call {@link #getUpdatedMediaPeriodInfo} to update period
- * information taking into account the new repeat mode.
- */
- public void setRepeatMode(@RepeatMode int repeatMode) {
- this.repeatMode = repeatMode;
- }
-
- /**
- * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period
- * information taking into account the shuffle mode.
- */
- public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
- this.shuffleModeEnabled = shuffleModeEnabled;
- }
-
- /**
- * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
- */
- public MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
- return getMediaPeriodInfo(playbackInfo.periodId, playbackInfo.contentPositionUs,
- playbackInfo.startPositionUs);
- }
-
- /**
- * Returns the {@link MediaPeriodInfo} following {@code currentMediaPeriodInfo}.
- *
- * @param currentMediaPeriodInfo The current media period info.
- * @param rendererOffsetUs The current renderer offset in microseconds.
- * @param rendererPositionUs The current renderer position in microseconds.
- * @return The following media period info, or {@code null} if it is not yet possible to get the
- * next media period info.
- */
- public MediaPeriodInfo getNextMediaPeriodInfo(MediaPeriodInfo currentMediaPeriodInfo,
- long rendererOffsetUs, long rendererPositionUs) {
- // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod
- // but if the timeline is not ready to provide the next period it can't return a non-null value
- // until the timeline is updated. Store whether the next timeline period is ready when the
- // timeline is updated, to avoid repeatedly checking the same timeline.
- if (currentMediaPeriodInfo.isLastInTimelinePeriod) {
- int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex,
- period, window, repeatMode, shuffleModeEnabled);
- if (nextPeriodIndex == C.INDEX_UNSET) {
- // We can't create a next period yet.
- return null;
- }
-
- long startPositionUs;
- int nextWindowIndex = timeline.getPeriod(nextPeriodIndex, period).windowIndex;
- if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
- // We're starting to buffer a new window. When playback transitions to this window we'll
- // want it to be from its default start position. The expected delay until playback
- // transitions is equal the duration of media that's currently buffered (assuming no
- // interruptions). Hence we project the default start position forward by the duration of
- // the buffer, and start buffering from this point.
- long defaultPositionProjectionUs =
- rendererOffsetUs + currentMediaPeriodInfo.durationUs - rendererPositionUs;
- Pair defaultPosition = timeline.getPeriodPosition(window, period,
- nextWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs));
- if (defaultPosition == null) {
- return null;
- }
- nextPeriodIndex = defaultPosition.first;
- startPositionUs = defaultPosition.second;
- } else {
- startPositionUs = 0;
- }
- MediaPeriodId periodId = resolvePeriodPositionForAds(nextPeriodIndex, startPositionUs);
- return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs);
- }
-
- MediaPeriodId currentPeriodId = currentMediaPeriodInfo.id;
- if (currentPeriodId.isAd()) {
- int currentAdGroupIndex = currentPeriodId.adGroupIndex;
- timeline.getPeriod(currentPeriodId.periodIndex, period);
- int adCountInCurrentAdGroup = period.getAdCountInAdGroup(currentAdGroupIndex);
- if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
- return null;
- }
- int nextAdIndexInAdGroup = currentPeriodId.adIndexInAdGroup + 1;
- if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
- // Play the next ad in the ad group if it's available.
- return !period.isAdAvailable(currentAdGroupIndex, nextAdIndexInAdGroup) ? null
- : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, currentAdGroupIndex,
- nextAdIndexInAdGroup, currentMediaPeriodInfo.contentPositionUs);
- } else {
- // Play content from the ad group position.
- int nextAdGroupIndex =
- period.getAdGroupIndexAfterPositionUs(currentMediaPeriodInfo.contentPositionUs);
- long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE
- : period.getAdGroupTimeUs(nextAdGroupIndex);
- return getMediaPeriodInfoForContent(currentPeriodId.periodIndex,
- currentMediaPeriodInfo.contentPositionUs, endUs);
- }
- } else if (currentMediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
- // Play the next ad group if it's available.
- int nextAdGroupIndex =
- period.getAdGroupIndexForPositionUs(currentMediaPeriodInfo.endPositionUs);
- return !period.isAdAvailable(nextAdGroupIndex, 0) ? null
- : getMediaPeriodInfoForAd(currentPeriodId.periodIndex, nextAdGroupIndex, 0,
- currentMediaPeriodInfo.endPositionUs);
- } else {
- // Check if the postroll ad should be played.
- int adGroupCount = period.getAdGroupCount();
- if (adGroupCount == 0
- || period.getAdGroupTimeUs(adGroupCount - 1) != C.TIME_END_OF_SOURCE
- || period.hasPlayedAdGroup(adGroupCount - 1)
- || !period.isAdAvailable(adGroupCount - 1, 0)) {
- return null;
- }
- long contentDurationUs = period.getDurationUs();
- return getMediaPeriodInfoForAd(currentPeriodId.periodIndex, adGroupCount - 1, 0,
- contentDurationUs);
- }
- }
-
- /**
- * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
- * played, returning an identifier for an ad group if one needs to be played before the specified
- * position, or an identifier for a content media period if not.
- */
- public MediaPeriodId resolvePeriodPositionForAds(int periodIndex, long positionUs) {
- timeline.getPeriod(periodIndex, period);
- int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
- if (adGroupIndex == C.INDEX_UNSET) {
- return new MediaPeriodId(periodIndex);
- } else {
- int adIndexInAdGroup = period.getPlayedAdCount(adGroupIndex);
- return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
- }
- }
-
- /**
- * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline.
- */
- public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo) {
- return getUpdatedMediaPeriodInfo(mediaPeriodInfo, mediaPeriodInfo.id);
- }
-
- /**
- * Returns the {@code mediaPeriodInfo} updated to take into account the current timeline,
- * resetting the identifier of the media period to the specified {@code newPeriodIndex}.
- */
- public MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo mediaPeriodInfo,
- int newPeriodIndex) {
- return getUpdatedMediaPeriodInfo(mediaPeriodInfo,
- mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex));
- }
-
- // Internal methods.
-
- private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) {
- long startPositionUs = info.startPositionUs;
- long endPositionUs = info.endPositionUs;
- boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs);
- boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod);
- timeline.getPeriod(newId.periodIndex, period);
- long durationUs = newId.isAd()
- ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup)
- : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs);
- return new MediaPeriodInfo(newId, startPositionUs, endPositionUs, info.contentPositionUs,
- durationUs, isLastInPeriod, isLastInTimeline);
- }
-
- private MediaPeriodInfo getMediaPeriodInfo(MediaPeriodId id, long contentPositionUs,
- long startPositionUs) {
- timeline.getPeriod(id.periodIndex, period);
- if (id.isAd()) {
- if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
- return null;
- }
- return getMediaPeriodInfoForAd(id.periodIndex, id.adGroupIndex, id.adIndexInAdGroup,
- contentPositionUs);
- } else {
- int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
- long endUs = nextAdGroupIndex == C.INDEX_UNSET ? C.TIME_END_OF_SOURCE
- : period.getAdGroupTimeUs(nextAdGroupIndex);
- return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, endUs);
- }
- }
-
- private MediaPeriodInfo getMediaPeriodInfoForAd(int periodIndex, int adGroupIndex,
- int adIndexInAdGroup, long contentPositionUs) {
- MediaPeriodId id = new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup);
- boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE);
- boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
- long durationUs = timeline.getPeriod(id.periodIndex, period)
- .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
- long startPositionUs = adIndexInAdGroup == period.getPlayedAdCount(adGroupIndex)
- ? period.getAdResumePositionUs() : 0;
- return new MediaPeriodInfo(id, startPositionUs, C.TIME_END_OF_SOURCE, contentPositionUs,
- durationUs, isLastInPeriod, isLastInTimeline);
- }
-
- private MediaPeriodInfo getMediaPeriodInfoForContent(int periodIndex, long startPositionUs,
- long endUs) {
- MediaPeriodId id = new MediaPeriodId(periodIndex);
- boolean isLastInPeriod = isLastInPeriod(id, endUs);
- boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
- timeline.getPeriod(id.periodIndex, period);
- long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
- return new MediaPeriodInfo(id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod,
- isLastInTimeline);
- }
-
- private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) {
- int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount();
- if (adGroupCount == 0) {
- return true;
- }
-
- int lastAdGroupIndex = adGroupCount - 1;
- boolean isAd = id.isAd();
- if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) {
- // There's no postroll ad.
- return !isAd && endPositionUs == C.TIME_END_OF_SOURCE;
- }
-
- int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex);
- if (postrollAdCount == C.LENGTH_UNSET) {
- // We won't know if this is the last ad until we know how many postroll ads there are.
- return false;
- }
-
- boolean isLastAd = isAd && id.adGroupIndex == lastAdGroupIndex
- && id.adIndexInAdGroup == postrollAdCount - 1;
- return isLastAd || (!isAd && period.getPlayedAdCount(lastAdGroupIndex) == postrollAdCount);
- }
-
- private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
- int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex;
- return !timeline.getWindow(windowIndex, window).isDynamic
- && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled)
- && isLastMediaPeriodInPeriod;
- }
-
-}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
new file mode 100644
index 0000000000..3efff58f5d
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java
@@ -0,0 +1,742 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
+import com.google.android.exoplayer2.upstream.Allocator;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Holds a queue of media periods, from the currently playing media period at the front to the
+ * loading media period at the end of the queue, with methods for controlling loading and updating
+ * the queue. Also has a reference to the media period currently being read.
+ */
+@SuppressWarnings("UngroupedOverloads")
+/* package */ final class MediaPeriodQueue {
+
+ /**
+ * Limits the maximum number of periods to buffer ahead of the current playing period. The
+ * buffering policy normally prevents buffering too far ahead, but the policy could allow too many
+ * small periods to be buffered if the period count were not limited.
+ */
+ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100;
+
+ private final Timeline.Period period;
+ private final Timeline.Window window;
+
+ private long nextWindowSequenceNumber;
+ private Timeline timeline;
+ private @RepeatMode int repeatMode;
+ private boolean shuffleModeEnabled;
+ private MediaPeriodHolder playing;
+ private MediaPeriodHolder reading;
+ private MediaPeriodHolder loading;
+ private int length;
+
+ /** Creates a new media period queue. */
+ public MediaPeriodQueue() {
+ period = new Timeline.Period();
+ window = new Timeline.Window();
+ }
+
+ /**
+ * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the
+ * queued media periods to take into account the new timeline.
+ */
+ public void setTimeline(Timeline timeline) {
+ this.timeline = timeline;
+ }
+
+ /**
+ * Sets the {@link RepeatMode} and returns whether the repeat mode change has been fully handled.
+ * If not, it is necessary to seek to the current playback position.
+ */
+ public boolean updateRepeatMode(@RepeatMode int repeatMode) {
+ this.repeatMode = repeatMode;
+ return updateForPlaybackModeChange();
+ }
+
+ /**
+ * Sets whether shuffling is enabled and returns whether the shuffle mode change has been fully
+ * handled. If not, it is necessary to seek to the current playback position.
+ */
+ public boolean updateShuffleModeEnabled(boolean shuffleModeEnabled) {
+ this.shuffleModeEnabled = shuffleModeEnabled;
+ return updateForPlaybackModeChange();
+ }
+
+ /** Returns whether {@code mediaPeriod} is the current loading media period. */
+ public boolean isLoading(MediaPeriod mediaPeriod) {
+ return loading != null && loading.mediaPeriod == mediaPeriod;
+ }
+
+ /**
+ * If there is a loading period, reevaluates its buffer.
+ *
+ * @param rendererPositionUs The current renderer position.
+ */
+ public void reevaluateBuffer(long rendererPositionUs) {
+ if (loading != null) {
+ loading.reevaluateBuffer(rendererPositionUs);
+ }
+ }
+
+ /** Returns whether a new loading media period should be enqueued, if available. */
+ public boolean shouldLoadNextMediaPeriod() {
+ return loading == null
+ || (!loading.info.isFinal
+ && loading.isFullyBuffered()
+ && loading.info.durationUs != C.TIME_UNSET
+ && length < MAXIMUM_BUFFER_AHEAD_PERIODS);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} for the next media period to load.
+ *
+ * @param rendererPositionUs The current renderer position.
+ * @param playbackInfo The current playback information.
+ * @return The {@link MediaPeriodInfo} for the next media period to load, or {@code null} if not
+ * yet known.
+ */
+ public @Nullable MediaPeriodInfo getNextMediaPeriodInfo(
+ long rendererPositionUs, PlaybackInfo playbackInfo) {
+ return loading == null
+ ? getFirstMediaPeriodInfo(playbackInfo)
+ : getFollowingMediaPeriodInfo(loading, rendererPositionUs);
+ }
+
+ /**
+ * Enqueues a new media period based on the specified information as the new loading media period,
+ * and returns it.
+ *
+ * @param rendererCapabilities The renderer capabilities.
+ * @param rendererTimestampOffsetUs The base time offset added to for renderers.
+ * @param trackSelector The track selector.
+ * @param allocator The allocator.
+ * @param mediaSource The media source that produced the media period.
+ * @param uid The unique identifier for the containing timeline period.
+ * @param info Information used to identify this media period in its timeline period.
+ */
+ public MediaPeriod enqueueNextMediaPeriod(
+ RendererCapabilities[] rendererCapabilities,
+ long rendererTimestampOffsetUs,
+ TrackSelector trackSelector,
+ Allocator allocator,
+ MediaSource mediaSource,
+ Object uid,
+ MediaPeriodInfo info) {
+ long rendererPositionOffsetUs =
+ loading == null
+ ? (info.startPositionUs + rendererTimestampOffsetUs)
+ : (loading.getRendererOffset() + loading.info.durationUs);
+ MediaPeriodHolder newPeriodHolder =
+ new MediaPeriodHolder(
+ rendererCapabilities,
+ rendererPositionOffsetUs,
+ trackSelector,
+ allocator,
+ mediaSource,
+ uid,
+ info);
+ if (loading != null) {
+ Assertions.checkState(hasPlayingPeriod());
+ loading.next = newPeriodHolder;
+ }
+ loading = newPeriodHolder;
+ length++;
+ return newPeriodHolder.mediaPeriod;
+ }
+
+ /**
+ * Handles the loading media period being prepared.
+ *
+ * @param playbackSpeed The current playback speed.
+ * @return The result of selecting tracks on the newly prepared loading media period.
+ */
+ public TrackSelectorResult handleLoadingPeriodPrepared(float playbackSpeed)
+ throws ExoPlaybackException {
+ return loading.handlePrepared(playbackSpeed);
+ }
+
+ /**
+ * Returns the loading period holder which is at the end of the queue, or null if the queue is
+ * empty.
+ */
+ public MediaPeriodHolder getLoadingPeriod() {
+ return loading;
+ }
+
+ /**
+ * Returns the playing period holder which is at the front of the queue, or null if the queue is
+ * empty or hasn't started playing.
+ */
+ public MediaPeriodHolder getPlayingPeriod() {
+ return playing;
+ }
+
+ /**
+ * Returns the reading period holder, or null if the queue is empty or the player hasn't started
+ * reading.
+ */
+ public MediaPeriodHolder getReadingPeriod() {
+ return reading;
+ }
+
+ /**
+ * Returns the period holder in the front of the queue which is the playing period holder when
+ * playing, or null if the queue is empty.
+ */
+ public MediaPeriodHolder getFrontPeriod() {
+ return hasPlayingPeriod() ? playing : loading;
+ }
+
+ /** Returns whether the reading and playing period holders are set. */
+ public boolean hasPlayingPeriod() {
+ return playing != null;
+ }
+
+ /**
+ * Continues reading from the next period holder in the queue.
+ *
+ * @return The updated reading period holder.
+ */
+ public MediaPeriodHolder advanceReadingPeriod() {
+ Assertions.checkState(reading != null && reading.next != null);
+ reading = reading.next;
+ return reading;
+ }
+
+ /**
+ * Dequeues the playing period holder from the front of the queue and advances the playing period
+ * holder to be the next item in the queue. If the playing period holder is unset, set it to the
+ * item in the front of the queue.
+ *
+ * @return The updated playing period holder, or null if the queue is or becomes empty.
+ */
+ public MediaPeriodHolder advancePlayingPeriod() {
+ if (playing != null) {
+ if (playing == reading) {
+ reading = playing.next;
+ }
+ playing.release();
+ playing = playing.next;
+ length--;
+ if (length == 0) {
+ loading = null;
+ }
+ } else {
+ playing = loading;
+ reading = loading;
+ }
+ return playing;
+ }
+
+ /**
+ * Removes all period holders after the given period holder. This process may also remove the
+ * currently reading period holder. If that is the case, the reading period holder is set to be
+ * the same as the playing period holder at the front of the queue.
+ *
+ * @param mediaPeriodHolder The media period holder that shall be the new end of the queue.
+ * @return Whether the reading period has been removed.
+ */
+ public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) {
+ Assertions.checkState(mediaPeriodHolder != null);
+ boolean removedReading = false;
+ loading = mediaPeriodHolder;
+ while (mediaPeriodHolder.next != null) {
+ mediaPeriodHolder = mediaPeriodHolder.next;
+ if (mediaPeriodHolder == reading) {
+ reading = playing;
+ removedReading = true;
+ }
+ mediaPeriodHolder.release();
+ length--;
+ }
+ loading.next = null;
+ return removedReading;
+ }
+
+ /** Clears the queue. */
+ public void clear() {
+ MediaPeriodHolder front = getFrontPeriod();
+ if (front != null) {
+ front.release();
+ removeAfter(front);
+ }
+ playing = null;
+ loading = null;
+ reading = null;
+ length = 0;
+ }
+
+ /**
+ * Updates media periods in the queue to take into account the latest timeline, and returns
+ * whether the timeline change has been fully handled. If not, it is necessary to seek to the
+ * current playback position. The method assumes that the first media period in the queue is still
+ * consistent with the new timeline.
+ *
+ * @param playingPeriodId The current playing media period identifier.
+ * @param rendererPositionUs The current renderer position in microseconds.
+ * @return Whether the timeline change has been handled completely.
+ */
+ public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) {
+ // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline
+ // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be
+ // handled here.
+ int periodIndex = playingPeriodId.periodIndex;
+ // The front period is either playing now, or is being loaded and will become the playing
+ // period.
+ MediaPeriodHolder previousPeriodHolder = null;
+ MediaPeriodHolder periodHolder = getFrontPeriod();
+ while (periodHolder != null) {
+ if (previousPeriodHolder == null) {
+ periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
+ } else {
+ // Check this period holder still follows the previous one, based on the new timeline.
+ if (periodIndex == C.INDEX_UNSET
+ || !periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) {
+ // The holder uid is inconsistent with the new timeline.
+ return !removeAfter(previousPeriodHolder);
+ }
+ MediaPeriodInfo periodInfo =
+ getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs);
+ if (periodInfo == null) {
+ // We've loaded a next media period that is not in the new timeline.
+ return !removeAfter(previousPeriodHolder);
+ }
+ // Update the period index.
+ periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info, periodIndex);
+ // Check the media period information matches the new timeline.
+ if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) {
+ return !removeAfter(previousPeriodHolder);
+ }
+ }
+
+ if (periodHolder.info.isLastInTimelinePeriod) {
+ // Move on to the next timeline period index, if there is one.
+ periodIndex =
+ timeline.getNextPeriodIndex(
+ periodIndex, period, window, repeatMode, shuffleModeEnabled);
+ }
+
+ previousPeriodHolder = periodHolder;
+ periodHolder = periodHolder.next;
+ }
+ return true;
+ }
+
+ /**
+ * Returns new media period info based on specified {@code mediaPeriodInfo} but taking into
+ * account the current timeline, and with the period index updated to {@code newPeriodIndex}.
+ *
+ * @param mediaPeriodInfo Media period info for a media period based on an old timeline.
+ * @param newPeriodIndex The new period index in the new timeline for the existing media period.
+ * @return The updated media period info for the current timeline.
+ */
+ public MediaPeriodInfo getUpdatedMediaPeriodInfo(
+ MediaPeriodInfo mediaPeriodInfo, int newPeriodIndex) {
+ return getUpdatedMediaPeriodInfo(
+ mediaPeriodInfo, mediaPeriodInfo.id.copyWithPeriodIndex(newPeriodIndex));
+ }
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * played, returning an identifier for an ad group if one needs to be played before the specified
+ * position, or an identifier for a content media period if not.
+ *
+ * @param periodIndex The index of the timeline period to play.
+ * @param positionUs The next content position in the period to play.
+ * @return The identifier for the first media period to play, taking into account unplayed ads.
+ */
+ public MediaPeriodId resolveMediaPeriodIdForAds(int periodIndex, long positionUs) {
+ long windowSequenceNumber = resolvePeriodIndexToWindowSequenceNumber(periodIndex);
+ return resolveMediaPeriodIdForAds(periodIndex, positionUs, windowSequenceNumber);
+ }
+
+ // Internal methods.
+
+ /**
+ * Resolves the specified timeline period and position to a {@link MediaPeriodId} that should be
+ * played, returning an identifier for an ad group if one needs to be played before the specified
+ * position, or an identifier for a content media period if not.
+ *
+ * @param periodIndex The index of the timeline period to play.
+ * @param positionUs The next content position in the period to play.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this period is part of.
+ * @return The identifier for the first media period to play, taking into account unplayed ads.
+ */
+ private MediaPeriodId resolveMediaPeriodIdForAds(
+ int periodIndex, long positionUs, long windowSequenceNumber) {
+ timeline.getPeriod(periodIndex, period);
+ int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs);
+ if (adGroupIndex == C.INDEX_UNSET) {
+ return new MediaPeriodId(periodIndex, windowSequenceNumber);
+ } else {
+ int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
+ return new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
+ }
+ }
+
+ /**
+ * Resolves the specified period index to a corresponding window sequence number. Either by
+ * reusing the window sequence number of an existing matching media period or by creating a new
+ * window sequence number.
+ *
+ * @param periodIndex The index of the timeline period.
+ * @return A window sequence number for a media period created for this timeline period.
+ */
+ private long resolvePeriodIndexToWindowSequenceNumber(int periodIndex) {
+ Object periodUid = timeline.getPeriod(periodIndex, period, /* setIds= */ true).uid;
+ MediaPeriodHolder mediaPeriodHolder = getFrontPeriod();
+ while (mediaPeriodHolder != null) {
+ if (mediaPeriodHolder.uid.equals(periodUid)) {
+ // Reuse window sequence number of first exact period match.
+ return mediaPeriodHolder.info.id.windowSequenceNumber;
+ }
+ mediaPeriodHolder = mediaPeriodHolder.next;
+ }
+ int windowIndex = period.windowIndex;
+ mediaPeriodHolder = getFrontPeriod();
+ while (mediaPeriodHolder != null) {
+ int indexOfHolderInTimeline = timeline.getIndexOfPeriod(mediaPeriodHolder.uid);
+ if (indexOfHolderInTimeline != C.INDEX_UNSET) {
+ int holderWindowIndex = timeline.getPeriod(indexOfHolderInTimeline, period).windowIndex;
+ if (holderWindowIndex == windowIndex) {
+ // As an alternative, try to match other periods of the same window.
+ return mediaPeriodHolder.info.id.windowSequenceNumber;
+ }
+ }
+ mediaPeriodHolder = mediaPeriodHolder.next;
+ }
+ // If no match is found, create new sequence number.
+ return nextWindowSequenceNumber++;
+ }
+
+ /**
+ * Returns whether {@code periodHolder} can be kept for playing the media period described by
+ * {@code info}.
+ */
+ private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) {
+ MediaPeriodInfo periodHolderInfo = periodHolder.info;
+ return periodHolderInfo.startPositionUs == info.startPositionUs
+ && periodHolderInfo.endPositionUs == info.endPositionUs
+ && periodHolderInfo.id.equals(info.id);
+ }
+
+ /**
+ * Updates the queue for any playback mode change, and returns whether the change was fully
+ * handled. If not, it is necessary to seek to the current playback position.
+ */
+ private boolean updateForPlaybackModeChange() {
+ // Find the last existing period holder that matches the new period order.
+ MediaPeriodHolder lastValidPeriodHolder = getFrontPeriod();
+ if (lastValidPeriodHolder == null) {
+ return true;
+ }
+ while (true) {
+ int nextPeriodIndex =
+ timeline.getNextPeriodIndex(
+ lastValidPeriodHolder.info.id.periodIndex,
+ period,
+ window,
+ repeatMode,
+ shuffleModeEnabled);
+ while (lastValidPeriodHolder.next != null
+ && !lastValidPeriodHolder.info.isLastInTimelinePeriod) {
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+ if (nextPeriodIndex == C.INDEX_UNSET
+ || lastValidPeriodHolder.next == null
+ || lastValidPeriodHolder.next.info.id.periodIndex != nextPeriodIndex) {
+ break;
+ }
+ lastValidPeriodHolder = lastValidPeriodHolder.next;
+ }
+
+ // Release any period holders that don't match the new period order.
+ boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder);
+
+ // Update the period info for the last holder, as it may now be the last period in the timeline.
+ lastValidPeriodHolder.info =
+ getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info, lastValidPeriodHolder.info.id);
+
+ // If renderers may have read from a period that's been removed, it is necessary to restart.
+ return !readingPeriodRemoved || !hasPlayingPeriod();
+ }
+
+ /**
+ * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position.
+ */
+ private MediaPeriodInfo getFirstMediaPeriodInfo(PlaybackInfo playbackInfo) {
+ return getMediaPeriodInfo(
+ playbackInfo.periodId, playbackInfo.contentPositionUs, playbackInfo.startPositionUs);
+ }
+
+ /**
+ * Returns the {@link MediaPeriodInfo} for the media period following {@code mediaPeriodHolder}'s
+ * media period.
+ *
+ * @param mediaPeriodHolder The media period holder.
+ * @param rendererPositionUs The current renderer position in microseconds.
+ * @return The following media period's info, or {@code null} if it is not yet possible to get the
+ * next media period info.
+ */
+ private @Nullable MediaPeriodInfo getFollowingMediaPeriodInfo(
+ MediaPeriodHolder mediaPeriodHolder, long rendererPositionUs) {
+ // TODO: This method is called repeatedly from ExoPlayerImplInternal.maybeUpdateLoadingPeriod
+ // but if the timeline is not ready to provide the next period it can't return a non-null value
+ // until the timeline is updated. Store whether the next timeline period is ready when the
+ // timeline is updated, to avoid repeatedly checking the same timeline.
+ MediaPeriodInfo mediaPeriodInfo = mediaPeriodHolder.info;
+ if (mediaPeriodInfo.isLastInTimelinePeriod) {
+ int nextPeriodIndex =
+ timeline.getNextPeriodIndex(
+ mediaPeriodInfo.id.periodIndex, period, window, repeatMode, shuffleModeEnabled);
+ if (nextPeriodIndex == C.INDEX_UNSET) {
+ // We can't create a next period yet.
+ return null;
+ }
+
+ long startPositionUs;
+ int nextWindowIndex =
+ timeline.getPeriod(nextPeriodIndex, period, /* setIds= */ true).windowIndex;
+ Object nextPeriodUid = period.uid;
+ long windowSequenceNumber = mediaPeriodInfo.id.windowSequenceNumber;
+ if (timeline.getWindow(nextWindowIndex, window).firstPeriodIndex == nextPeriodIndex) {
+ // We're starting to buffer a new window. When playback transitions to this window we'll
+ // want it to be from its default start position. The expected delay until playback
+ // transitions is equal the duration of media that's currently buffered (assuming no
+ // interruptions). Hence we project the default start position forward by the duration of
+ // the buffer, and start buffering from this point.
+ long defaultPositionProjectionUs =
+ mediaPeriodHolder.getRendererOffset() + mediaPeriodInfo.durationUs - rendererPositionUs;
+ Pair defaultPosition =
+ timeline.getPeriodPosition(
+ window,
+ period,
+ nextWindowIndex,
+ C.TIME_UNSET,
+ Math.max(0, defaultPositionProjectionUs));
+ if (defaultPosition == null) {
+ return null;
+ }
+ nextPeriodIndex = defaultPosition.first;
+ startPositionUs = defaultPosition.second;
+ if (mediaPeriodHolder.next != null && mediaPeriodHolder.next.uid.equals(nextPeriodUid)) {
+ windowSequenceNumber = mediaPeriodHolder.next.info.id.windowSequenceNumber;
+ } else {
+ windowSequenceNumber = nextWindowSequenceNumber++;
+ }
+ } else {
+ startPositionUs = 0;
+ }
+ MediaPeriodId periodId =
+ resolveMediaPeriodIdForAds(nextPeriodIndex, startPositionUs, windowSequenceNumber);
+ return getMediaPeriodInfo(periodId, startPositionUs, startPositionUs);
+ }
+
+ MediaPeriodId currentPeriodId = mediaPeriodInfo.id;
+ timeline.getPeriod(currentPeriodId.periodIndex, period);
+ if (currentPeriodId.isAd()) {
+ int adGroupIndex = currentPeriodId.adGroupIndex;
+ int adCountInCurrentAdGroup = period.getAdCountInAdGroup(adGroupIndex);
+ if (adCountInCurrentAdGroup == C.LENGTH_UNSET) {
+ return null;
+ }
+ int nextAdIndexInAdGroup =
+ period.getNextAdIndexToPlay(adGroupIndex, currentPeriodId.adIndexInAdGroup);
+ if (nextAdIndexInAdGroup < adCountInCurrentAdGroup) {
+ // Play the next ad in the ad group if it's available.
+ return !period.isAdAvailable(adGroupIndex, nextAdIndexInAdGroup)
+ ? null
+ : getMediaPeriodInfoForAd(
+ currentPeriodId.periodIndex,
+ adGroupIndex,
+ nextAdIndexInAdGroup,
+ mediaPeriodInfo.contentPositionUs,
+ currentPeriodId.windowSequenceNumber);
+ } else {
+ // Play content from the ad group position.
+ return getMediaPeriodInfoForContent(
+ currentPeriodId.periodIndex,
+ mediaPeriodInfo.contentPositionUs,
+ currentPeriodId.windowSequenceNumber);
+ }
+ } else if (mediaPeriodInfo.endPositionUs != C.TIME_END_OF_SOURCE) {
+ // Play the next ad group if it's available.
+ int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs);
+ if (nextAdGroupIndex == C.INDEX_UNSET) {
+ // The next ad group can't be played. Play content from the ad group position instead.
+ return getMediaPeriodInfoForContent(
+ currentPeriodId.periodIndex,
+ mediaPeriodInfo.endPositionUs,
+ currentPeriodId.windowSequenceNumber);
+ }
+ int adIndexInAdGroup = period.getFirstAdIndexToPlay(nextAdGroupIndex);
+ return !period.isAdAvailable(nextAdGroupIndex, adIndexInAdGroup)
+ ? null
+ : getMediaPeriodInfoForAd(
+ currentPeriodId.periodIndex,
+ nextAdGroupIndex,
+ adIndexInAdGroup,
+ mediaPeriodInfo.endPositionUs,
+ currentPeriodId.windowSequenceNumber);
+ } else {
+ // Check if the postroll ad should be played.
+ int adGroupCount = period.getAdGroupCount();
+ if (adGroupCount == 0) {
+ return null;
+ }
+ int adGroupIndex = adGroupCount - 1;
+ if (period.getAdGroupTimeUs(adGroupIndex) != C.TIME_END_OF_SOURCE
+ || period.hasPlayedAdGroup(adGroupIndex)) {
+ return null;
+ }
+ int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex);
+ if (!period.isAdAvailable(adGroupIndex, adIndexInAdGroup)) {
+ return null;
+ }
+ long contentDurationUs = period.getDurationUs();
+ return getMediaPeriodInfoForAd(
+ currentPeriodId.periodIndex,
+ adGroupIndex,
+ adIndexInAdGroup,
+ contentDurationUs,
+ currentPeriodId.windowSequenceNumber);
+ }
+ }
+
+ private MediaPeriodInfo getUpdatedMediaPeriodInfo(MediaPeriodInfo info, MediaPeriodId newId) {
+ long startPositionUs = info.startPositionUs;
+ long endPositionUs = info.endPositionUs;
+ boolean isLastInPeriod = isLastInPeriod(newId, endPositionUs);
+ boolean isLastInTimeline = isLastInTimeline(newId, isLastInPeriod);
+ timeline.getPeriod(newId.periodIndex, period);
+ long durationUs =
+ newId.isAd()
+ ? period.getAdDurationUs(newId.adGroupIndex, newId.adIndexInAdGroup)
+ : (endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endPositionUs);
+ return new MediaPeriodInfo(
+ newId,
+ startPositionUs,
+ endPositionUs,
+ info.contentPositionUs,
+ durationUs,
+ isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfo(
+ MediaPeriodId id, long contentPositionUs, long startPositionUs) {
+ timeline.getPeriod(id.periodIndex, period);
+ if (id.isAd()) {
+ if (!period.isAdAvailable(id.adGroupIndex, id.adIndexInAdGroup)) {
+ return null;
+ }
+ return getMediaPeriodInfoForAd(
+ id.periodIndex,
+ id.adGroupIndex,
+ id.adIndexInAdGroup,
+ contentPositionUs,
+ id.windowSequenceNumber);
+ } else {
+ return getMediaPeriodInfoForContent(id.periodIndex, startPositionUs, id.windowSequenceNumber);
+ }
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForAd(
+ int periodIndex,
+ int adGroupIndex,
+ int adIndexInAdGroup,
+ long contentPositionUs,
+ long windowSequenceNumber) {
+ MediaPeriodId id =
+ new MediaPeriodId(periodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
+ boolean isLastInPeriod = isLastInPeriod(id, C.TIME_END_OF_SOURCE);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ long durationUs =
+ timeline
+ .getPeriod(id.periodIndex, period)
+ .getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup);
+ long startPositionUs =
+ adIndexInAdGroup == period.getFirstAdIndexToPlay(adGroupIndex)
+ ? period.getAdResumePositionUs()
+ : 0;
+ return new MediaPeriodInfo(
+ id,
+ startPositionUs,
+ C.TIME_END_OF_SOURCE,
+ contentPositionUs,
+ durationUs,
+ isLastInPeriod,
+ isLastInTimeline);
+ }
+
+ private MediaPeriodInfo getMediaPeriodInfoForContent(
+ int periodIndex, long startPositionUs, long windowSequenceNumber) {
+ MediaPeriodId id = new MediaPeriodId(periodIndex, windowSequenceNumber);
+ timeline.getPeriod(id.periodIndex, period);
+ int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs);
+ long endUs =
+ nextAdGroupIndex == C.INDEX_UNSET
+ ? C.TIME_END_OF_SOURCE
+ : period.getAdGroupTimeUs(nextAdGroupIndex);
+ boolean isLastInPeriod = isLastInPeriod(id, endUs);
+ boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod);
+ long durationUs = endUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() : endUs;
+ return new MediaPeriodInfo(
+ id, startPositionUs, endUs, C.TIME_UNSET, durationUs, isLastInPeriod, isLastInTimeline);
+ }
+
+ private boolean isLastInPeriod(MediaPeriodId id, long endPositionUs) {
+ int adGroupCount = timeline.getPeriod(id.periodIndex, period).getAdGroupCount();
+ if (adGroupCount == 0) {
+ return true;
+ }
+
+ int lastAdGroupIndex = adGroupCount - 1;
+ boolean isAd = id.isAd();
+ if (period.getAdGroupTimeUs(lastAdGroupIndex) != C.TIME_END_OF_SOURCE) {
+ // There's no postroll ad.
+ return !isAd && endPositionUs == C.TIME_END_OF_SOURCE;
+ }
+
+ int postrollAdCount = period.getAdCountInAdGroup(lastAdGroupIndex);
+ if (postrollAdCount == C.LENGTH_UNSET) {
+ // We won't know if this is the last ad until we know how many postroll ads there are.
+ return false;
+ }
+
+ boolean isLastAd =
+ isAd && id.adGroupIndex == lastAdGroupIndex && id.adIndexInAdGroup == postrollAdCount - 1;
+ return isLastAd || (!isAd && period.getFirstAdIndexToPlay(lastAdGroupIndex) == postrollAdCount);
+ }
+
+ private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) {
+ int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex;
+ return !timeline.getWindow(windowIndex, window).isDynamic
+ && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled)
+ && isLastMediaPeriodInPeriod;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
index 978f4f7a97..593d3d1fce 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java
@@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities
return ADAPTIVE_NOT_SUPPORTED;
}
- // ExoPlayerComponent implementation.
+ // PlayerMessage.Target implementation.
@Override
public void handleMessage(int what, Object object) throws ExoPlaybackException {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
index a2ffa43c4b..3ff2ec9461 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java
@@ -15,57 +15,145 @@
*/
package com.google.android.exoplayer2;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
+import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
/**
* Information about an ongoing playback.
*/
/* package */ final class PlaybackInfo {
- public final Timeline timeline;
- public final Object manifest;
+ public final @Nullable Timeline timeline;
+ public final @Nullable Object manifest;
public final MediaPeriodId periodId;
public final long startPositionUs;
public final long contentPositionUs;
+ public final int playbackState;
+ public final boolean isLoading;
+ public final TrackSelectorResult trackSelectorResult;
public volatile long positionUs;
public volatile long bufferedPositionUs;
- public PlaybackInfo(Timeline timeline, Object manifest, int periodIndex, long startPositionUs) {
- this(timeline, manifest, new MediaPeriodId(periodIndex), startPositionUs, C.TIME_UNSET);
+ public PlaybackInfo(
+ @Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) {
+ this(
+ timeline,
+ /* manifest= */ null,
+ new MediaPeriodId(/* periodIndex= */ 0),
+ startPositionUs,
+ /* contentPositionUs =*/ C.TIME_UNSET,
+ Player.STATE_IDLE,
+ /* isLoading= */ false,
+ trackSelectorResult);
}
- public PlaybackInfo(Timeline timeline, Object manifest, MediaPeriodId periodId,
- long startPositionUs, long contentPositionUs) {
+ public PlaybackInfo(
+ @Nullable Timeline timeline,
+ @Nullable Object manifest,
+ MediaPeriodId periodId,
+ long startPositionUs,
+ long contentPositionUs,
+ int playbackState,
+ boolean isLoading,
+ TrackSelectorResult trackSelectorResult) {
this.timeline = timeline;
this.manifest = manifest;
this.periodId = periodId;
this.startPositionUs = startPositionUs;
this.contentPositionUs = contentPositionUs;
- positionUs = startPositionUs;
- bufferedPositionUs = startPositionUs;
- }
-
- public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs,
- long contentPositionUs) {
- return fromNewPosition(new MediaPeriodId(periodIndex), startPositionUs, contentPositionUs);
+ this.positionUs = startPositionUs;
+ this.bufferedPositionUs = startPositionUs;
+ this.playbackState = playbackState;
+ this.isLoading = isLoading;
+ this.trackSelectorResult = trackSelectorResult;
}
public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs,
long contentPositionUs) {
- return new PlaybackInfo(timeline, manifest, periodId, startPositionUs, contentPositionUs);
+ return new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ periodId.isAd() ? contentPositionUs : C.TIME_UNSET,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
}
public PlaybackInfo copyWithPeriodIndex(int periodIndex) {
- PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest,
- periodId.copyWithPeriodIndex(periodIndex), startPositionUs, contentPositionUs);
+ PlaybackInfo playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId.copyWithPeriodIndex(periodIndex),
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
}
public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) {
- PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, periodId, startPositionUs,
- contentPositionUs);
+ PlaybackInfo playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
+ copyMutablePositions(this, playbackInfo);
+ return playbackInfo;
+ }
+
+ public PlaybackInfo copyWithPlaybackState(int playbackState) {
+ PlaybackInfo playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
+ copyMutablePositions(this, playbackInfo);
+ return playbackInfo;
+ }
+
+ public PlaybackInfo copyWithIsLoading(boolean isLoading) {
+ PlaybackInfo playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
+ copyMutablePositions(this, playbackInfo);
+ return playbackInfo;
+ }
+
+ public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) {
+ PlaybackInfo playbackInfo =
+ new PlaybackInfo(
+ timeline,
+ manifest,
+ periodId,
+ startPositionUs,
+ contentPositionUs,
+ playbackState,
+ isLoading,
+ trackSelectorResult);
copyMutablePositions(this, playbackInfo);
return playbackInfo;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
index 90aded7660..47d5bc88b9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
+import com.google.android.exoplayer2.util.Assertions;
+
/**
* The parameters that apply to playback.
*/
@@ -40,23 +42,25 @@ public final class PlaybackParameters {
/**
* Creates new playback parameters.
*
- * @param speed The factor by which playback will be sped up.
- * @param pitch The factor by which the audio pitch will be scaled.
+ * @param speed The factor by which playback will be sped up. Must be greater than zero.
+ * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero.
*/
public PlaybackParameters(float speed, float pitch) {
+ Assertions.checkArgument(speed > 0);
+ Assertions.checkArgument(pitch > 0);
this.speed = speed;
this.pitch = pitch;
scaledUsPerMs = Math.round(speed * 1000f);
}
/**
- * Scales the millisecond duration {@code timeMs} by the playback speed, returning the result in
- * microseconds.
+ * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of
+ * wallclock time.
*
* @param timeMs The time to scale, in milliseconds.
* @return The scaled time, in microseconds.
*/
- public long getSpeedAdjustedDurationUs(long timeMs) {
+ public long getMediaTimeUsForPlayoutTimeMs(long timeMs) {
return timeMs * scaledUsPerMs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java
new file mode 100644
index 0000000000..8ff7f50402
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 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;
+
+/** Called to prepare a playback. */
+public interface PlaybackPreparer {
+
+ /** Called to prepare a playback. */
+ void preparePlayback();
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
index d911f83392..443ff8a2ea 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java
@@ -18,8 +18,14 @@ package com.google.android.exoplayer2;
import android.os.Looper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
+import com.google.android.exoplayer2.video.VideoListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -44,6 +50,130 @@ import java.lang.annotation.RetentionPolicy;
*/
public interface Player {
+ /** The video component of a {@link Player}. */
+ interface VideoComponent {
+
+ /**
+ * Sets the video scaling mode.
+ *
+ * @param videoScalingMode The video scaling mode.
+ */
+ void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode);
+
+ /** Returns the video scaling mode. */
+ @C.VideoScalingMode
+ int getVideoScalingMode();
+
+ /**
+ * Adds a listener to receive video events.
+ *
+ * @param listener The listener to register.
+ */
+ void addVideoListener(VideoListener listener);
+
+ /**
+ * Removes a listener of video events.
+ *
+ * @param listener The listener to unregister.
+ */
+ void removeVideoListener(VideoListener listener);
+
+ /**
+ * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
+ * currently set on the player.
+ */
+ void clearVideoSurface();
+
+ /**
+ * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
+ * tracking the lifecycle of the surface, and must clear the surface by calling {@code
+ * setVideoSurface(null)} if the surface is destroyed.
+ *
+ * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link
+ * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link
+ * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather
+ * than this method, since passing the holder allows the player to track the lifecycle of the
+ * surface automatically.
+ *
+ * @param surface The {@link Surface}.
+ */
+ void setVideoSurface(Surface surface);
+
+ /**
+ * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
+ * Else does nothing.
+ *
+ * @param surface The surface to clear.
+ */
+ void clearVideoSurface(Surface surface);
+
+ /**
+ * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
+ * rendered. The player will track the lifecycle of the surface automatically.
+ *
+ * @param surfaceHolder The surface holder.
+ */
+ void setVideoSurfaceHolder(SurfaceHolder surfaceHolder);
+
+ /**
+ * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being
+ * rendered if it matches the one passed. Else does nothing.
+ *
+ * @param surfaceHolder The surface holder to clear.
+ */
+ void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder);
+
+ /**
+ * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param surfaceView The surface view.
+ */
+ void setVideoSurfaceView(SurfaceView surfaceView);
+
+ /**
+ * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one
+ * passed. Else does nothing.
+ *
+ * @param surfaceView The texture view to clear.
+ */
+ void clearVideoSurfaceView(SurfaceView surfaceView);
+
+ /**
+ * Sets the {@link TextureView} onto which video will be rendered. The player will track the
+ * lifecycle of the surface automatically.
+ *
+ * @param textureView The texture view.
+ */
+ void setVideoTextureView(TextureView textureView);
+
+ /**
+ * Clears the {@link TextureView} onto which video is being rendered if it matches the one
+ * passed. Else does nothing.
+ *
+ * @param textureView The texture view to clear.
+ */
+ void clearVideoTextureView(TextureView textureView);
+ }
+
+ /** The text component of a {@link Player}. */
+ interface TextComponent {
+
+ /**
+ * Registers an output to receive text events.
+ *
+ * @param listener The output to register.
+ */
+ void addTextOutput(TextOutput listener);
+
+ /**
+ * Removes a text output.
+ *
+ * @param listener The output to remove.
+ */
+ void removeTextOutput(TextOutput listener);
+ }
+
/**
* Listener of changes in player state.
*/
@@ -59,8 +189,9 @@ public interface Player {
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
+ * @param reason The {@link TimelineChangeReason} responsible for this timeline change.
*/
- void onTimelineChanged(Timeline timeline, Object manifest);
+ void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason);
/**
* Called when the available or selected tracks change.
@@ -118,7 +249,8 @@ public interface Player {
* when the source introduces a discontinuity internally).
*
* When a position discontinuity occurs as a result of a change to the timeline this method is
- * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
+ * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this
+ * case.
*
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
@@ -149,8 +281,10 @@ public interface Player {
abstract class DefaultEventListener implements EventListener {
@Override
- public void onTimelineChanged(Timeline timeline, Object manifest) {
- // Do nothing.
+ public void onTimelineChanged(Timeline timeline, Object manifest,
+ @TimelineChangeReason int reason) {
+ // Call deprecated version. Otherwise, do nothing.
+ onTimelineChanged(timeline, manifest);
}
@Override
@@ -198,6 +332,15 @@ public interface Player {
// Do nothing.
}
+ /**
+ * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)}
+ * instead.
+ */
+ @Deprecated
+ public void onTimelineChanged(Timeline timeline, Object manifest) {
+ // Do nothing.
+ }
+
}
/**
@@ -238,31 +381,60 @@ public interface Player {
*/
int REPEAT_MODE_ALL = 2;
- /**
- * Reasons for position discontinuities.
- */
+ /** Reasons for position discontinuities. */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK,
- DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL})
+ @IntDef({
+ DISCONTINUITY_REASON_PERIOD_TRANSITION,
+ DISCONTINUITY_REASON_SEEK,
+ DISCONTINUITY_REASON_SEEK_ADJUSTMENT,
+ DISCONTINUITY_REASON_AD_INSERTION,
+ DISCONTINUITY_REASON_INTERNAL
+ })
public @interface DiscontinuityReason {}
/**
* Automatic playback transition from one period in the timeline to the next. The period index may
* be the same as it was before the discontinuity in case the current period is repeated.
*/
int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0;
- /**
- * Seek within the current period or to another period.
- */
+ /** Seek within the current period or to another period. */
int DISCONTINUITY_REASON_SEEK = 1;
/**
* Seek adjustment due to being unable to seek to the requested position or because the seek was
* permitted to be inexact.
*/
int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2;
+ /** Discontinuity to or from an ad within one period in the timeline. */
+ int DISCONTINUITY_REASON_AD_INSERTION = 3;
+ /** Discontinuity introduced internally by the source. */
+ int DISCONTINUITY_REASON_INTERNAL = 4;
+
/**
- * Discontinuity introduced internally by the source.
+ * Reasons for timeline and/or manifest changes.
*/
- int DISCONTINUITY_REASON_INTERNAL = 3;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TIMELINE_CHANGE_REASON_PREPARED, TIMELINE_CHANGE_REASON_RESET,
+ TIMELINE_CHANGE_REASON_DYNAMIC})
+ public @interface TimelineChangeReason {}
+ /**
+ * Timeline and manifest changed as a result of a player initialization with new media.
+ */
+ int TIMELINE_CHANGE_REASON_PREPARED = 0;
+ /**
+ * Timeline and manifest changed as a result of a player reset.
+ */
+ int TIMELINE_CHANGE_REASON_RESET = 1;
+ /**
+ * Timeline or manifest changed as a result of an dynamic update introduced by the played media.
+ */
+ int TIMELINE_CHANGE_REASON_DYNAMIC = 2;
+
+ /** Returns the component of this player for video output, or null if video is not supported. */
+ @Nullable
+ VideoComponent getVideoComponent();
+
+ /** Returns the component of this player for text output, or null if text is not supported. */
+ @Nullable
+ TextComponent getTextComponent();
/**
* Register a listener to receive events from the player. The listener's methods will be called on
@@ -396,17 +568,29 @@ public interface Player {
PlaybackParameters getPlaybackParameters();
/**
- * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
- * is to pause playback.
- *
- * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
+ * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than
+ * this method if the intention is to pause playback.
+ *
+ *
Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
* player instance can still be used, and {@link #release()} must still be called on the player if
* it's no longer required.
- *
- * Calling this method does not reset the playback position.
+ *
+ *
Calling this method does not reset the playback position.
*/
void stop();
+ /**
+ * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather
+ * than this method if the intention is to pause playback.
+ *
+ *
Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
+ * player instance can still be used, and {@link #release()} must still be called on the player if
+ * it's no longer required.
+ *
+ * @param reset Whether the player should be reset.
+ */
+ void stop(boolean reset);
+
/**
* Releases the player. This method must be called when the player is no longer required. The
* player must not be used after calling this method.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
new file mode 100644
index 0000000000..1e8a89e102
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java
@@ -0,0 +1,280 @@
+/*
+ * 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;
+
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Defines a player message which can be sent with a {@link Sender} and received by a {@link
+ * Target}.
+ */
+public final class PlayerMessage {
+
+ /** A target for messages. */
+ public interface Target {
+
+ /**
+ * Handles a message delivered to the target.
+ *
+ * @param messageType The message type.
+ * @param payload The message payload.
+ * @throws ExoPlaybackException If an error occurred whilst handling the message.
+ */
+ void handleMessage(int messageType, Object payload) throws ExoPlaybackException;
+ }
+
+ /** A sender for messages. */
+ public interface Sender {
+
+ /**
+ * Sends a message.
+ *
+ * @param message The message to be sent.
+ */
+ void sendMessage(PlayerMessage message);
+ }
+
+ private final Target target;
+ private final Sender sender;
+ private final Timeline timeline;
+
+ private int type;
+ private Object payload;
+ private Handler handler;
+ private int windowIndex;
+ private long positionMs;
+ private boolean deleteAfterDelivery;
+ private boolean isSent;
+ private boolean isDelivered;
+ private boolean isProcessed;
+
+ /**
+ * Creates a new message.
+ *
+ * @param sender The {@link Sender} used to send the message.
+ * @param target The {@link Target} the message is sent to.
+ * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If
+ * set to {@link Timeline#EMPTY}, any position can be specified.
+ * @param defaultWindowIndex The default window index in the {@code timeline} when no other window
+ * index is specified.
+ * @param defaultHandler The default handler to send the message on when no other handler is
+ * specified.
+ */
+ public PlayerMessage(
+ Sender sender,
+ Target target,
+ Timeline timeline,
+ int defaultWindowIndex,
+ Handler defaultHandler) {
+ this.sender = sender;
+ this.target = target;
+ this.timeline = timeline;
+ this.handler = defaultHandler;
+ this.windowIndex = defaultWindowIndex;
+ this.positionMs = C.TIME_UNSET;
+ this.deleteAfterDelivery = true;
+ }
+
+ /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */
+ public Timeline getTimeline() {
+ return timeline;
+ }
+
+ /** Returns the target the message is sent to. */
+ public Target getTarget() {
+ return target;
+ }
+
+ /**
+ * Sets the message type forwarded to {@link Target#handleMessage(int, Object)}.
+ *
+ * @param messageType The message type.
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setType(int messageType) {
+ Assertions.checkState(!isSent);
+ this.type = messageType;
+ return this;
+ }
+
+ /** Returns the message type forwarded to {@link Target#handleMessage(int, Object)}. */
+ public int getType() {
+ return type;
+ }
+
+ /**
+ * Sets the message payload forwarded to {@link Target#handleMessage(int, Object)}.
+ *
+ * @param payload The message payload.
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setPayload(@Nullable Object payload) {
+ Assertions.checkState(!isSent);
+ this.payload = payload;
+ return this;
+ }
+
+ /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */
+ public Object getPayload() {
+ return payload;
+ }
+
+ /**
+ * Sets the handler the message is delivered on.
+ *
+ * @param handler A {@link Handler}.
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setHandler(Handler handler) {
+ Assertions.checkState(!isSent);
+ this.handler = handler;
+ return this;
+ }
+
+ /** Returns the handler the message is delivered on. */
+ public Handler getHandler() {
+ return handler;
+ }
+
+ /**
+ * Sets a position in the current window at which the message will be delivered.
+ *
+ * @param positionMs The position in the current window at which the message will be sent, in
+ * milliseconds.
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setPosition(long positionMs) {
+ Assertions.checkState(!isSent);
+ this.positionMs = positionMs;
+ return this;
+ }
+
+ /**
+ * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered,
+ * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately.
+ */
+ public long getPositionMs() {
+ return positionMs;
+ }
+
+ /**
+ * Sets a position in a window at which the message will be delivered.
+ *
+ * @param windowIndex The index of the window at which the message will be sent.
+ * @param positionMs The position in the window with index {@code windowIndex} at which the
+ * message will be sent, in milliseconds.
+ * @return This message.
+ * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not
+ * empty and the provided window index is not within the bounds of the timeline.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setPosition(int windowIndex, long positionMs) {
+ Assertions.checkState(!isSent);
+ Assertions.checkArgument(positionMs != C.TIME_UNSET);
+ if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
+ throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
+ }
+ this.windowIndex = windowIndex;
+ this.positionMs = positionMs;
+ return this;
+ }
+
+ /** Returns window index at which the message will be delivered. */
+ public int getWindowIndex() {
+ return windowIndex;
+ }
+
+ /**
+ * Sets whether the message will be deleted after delivery. If false, the message will be resent
+ * if playback reaches the specified position again. Only allowed to be false if a position is set
+ * with {@link #setPosition(long)}.
+ *
+ * @param deleteAfterDelivery Whether the message is deleted after delivery.
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) {
+ Assertions.checkState(!isSent);
+ this.deleteAfterDelivery = deleteAfterDelivery;
+ return this;
+ }
+
+ /** Returns whether the message will be deleted after delivery. */
+ public boolean getDeleteAfterDelivery() {
+ return deleteAfterDelivery;
+ }
+
+ /**
+ * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated
+ * out of the player as an error using {@link
+ * Player.EventListener#onPlayerError(ExoPlaybackException)}.
+ *
+ * @return This message.
+ * @throws IllegalStateException If {@link #send()} has already been called.
+ */
+ public PlayerMessage send() {
+ Assertions.checkState(!isSent);
+ if (positionMs == C.TIME_UNSET) {
+ Assertions.checkArgument(deleteAfterDelivery);
+ }
+ isSent = true;
+ sender.sendMessage(this);
+ return this;
+ }
+
+ /**
+ * Blocks until after the message has been delivered or the player is no longer able to deliver
+ * the message.
+ *
+ *
Note that this method can't be called if the current thread is the same thread used by the
+ * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock.
+ *
+ * @return Whether the message was delivered successfully.
+ * @throws IllegalStateException If this method is called before {@link #send()}.
+ * @throws IllegalStateException If this method is called on the same thread used by the message
+ * handler set with {@link #setHandler(Handler)}.
+ * @throws InterruptedException If the current thread is interrupted while waiting for the message
+ * to be delivered.
+ */
+ public synchronized boolean blockUntilDelivered() throws InterruptedException {
+ Assertions.checkState(isSent);
+ Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread());
+ while (!isProcessed) {
+ wait();
+ }
+ return isDelivered;
+ }
+
+ /**
+ * Marks the message as processed. Should only be called by a {@link Sender} and may be called
+ * multiple times.
+ *
+ * @param isDelivered Whether the message has been delivered to its target. The message is
+ * considered as being delivered when this method has been called with {@code isDelivered} set
+ * to true at least once.
+ */
+ public synchronized void markAsProcessed(boolean isDelivered) {
+ this.isDelivered |= isDelivered;
+ isProcessed = true;
+ notifyAll();
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
index 6def1591da..d0a07930e0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java
@@ -15,22 +15,20 @@
*/
package com.google.android.exoplayer2;
-import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.util.MediaClock;
import java.io.IOException;
/**
* Renders media read from a {@link SampleStream}.
- *
- * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
+ *
+ *
Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is
* transitioned through various states as the overall playback state changes. The valid state
* transitions are shown below, annotated with the methods that are called during each transition.
- *
- *
- *
+ *
+ *
*/
-public interface Renderer extends ExoPlayerComponent {
+public interface Renderer extends PlayerMessage.Target {
/**
* The renderer is disabled.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
new file mode 100644
index 0000000000..2df9840cf8
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java
@@ -0,0 +1,90 @@
+/*
+ * 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;
+
+import com.google.android.exoplayer2.util.Assertions;
+
+/**
+ * Parameters that apply to seeking.
+ *
+ *
The predefined {@link #EXACT}, {@link #CLOSEST_SYNC}, {@link #PREVIOUS_SYNC} and {@link
+ * #NEXT_SYNC} parameters are suitable for most use cases. Seeking to sync points is typically
+ * faster but less accurate than exact seeking.
+ *
+ *
In the general case, an instance specifies a maximum tolerance before ({@link
+ * #toleranceBeforeUs}) and after ({@link #toleranceAfterUs}) a requested seek position ({@code x}).
+ * If one or more sync points falls within the window {@code [x - toleranceBeforeUs, x +
+ * toleranceAfterUs]} then the seek will be performed to the sync point within the window that's
+ * closest to {@code x}. If no sync point falls within the window then the seek will be performed to
+ * {@code x - toleranceBeforeUs}. Internally the player may need to seek to an earlier sync point
+ * and discard media until this position is reached.
+ */
+public final class SeekParameters {
+
+ /** Parameters for exact seeking. */
+ public static final SeekParameters EXACT = new SeekParameters(0, 0);
+ /** Parameters for seeking to the closest sync point. */
+ public static final SeekParameters CLOSEST_SYNC =
+ new SeekParameters(Long.MAX_VALUE, Long.MAX_VALUE);
+ /** Parameters for seeking to the sync point immediately before a requested seek position. */
+ public static final SeekParameters PREVIOUS_SYNC = new SeekParameters(Long.MAX_VALUE, 0);
+ /** Parameters for seeking to the sync point immediately after a requested seek position. */
+ public static final SeekParameters NEXT_SYNC = new SeekParameters(0, Long.MAX_VALUE);
+ /** Default parameters. */
+ public static final SeekParameters DEFAULT = EXACT;
+
+ /**
+ * The maximum time that the actual position seeked to may precede the requested seek position, in
+ * microseconds.
+ */
+ public final long toleranceBeforeUs;
+ /**
+ * The maximum time that the actual position seeked to may exceed the requested seek position, in
+ * microseconds.
+ */
+ public final long toleranceAfterUs;
+
+ /**
+ * @param toleranceBeforeUs The maximum time that the actual position seeked to may precede the
+ * requested seek position, in microseconds. Must be non-negative.
+ * @param toleranceAfterUs The maximum time that the actual position seeked to may exceed the
+ * requested seek position, in microseconds. Must be non-negative.
+ */
+ public SeekParameters(long toleranceBeforeUs, long toleranceAfterUs) {
+ Assertions.checkArgument(toleranceBeforeUs >= 0);
+ Assertions.checkArgument(toleranceAfterUs >= 0);
+ this.toleranceBeforeUs = toleranceBeforeUs;
+ this.toleranceAfterUs = toleranceAfterUs;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekParameters other = (SeekParameters) obj;
+ return toleranceBeforeUs == other.toleranceBeforeUs
+ && toleranceAfterUs == other.toleranceAfterUs;
+ }
+
+ @Override
+ public int hashCode() {
+ return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
index 544b10b7ef..98ef35d62c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java
@@ -38,8 +38,10 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
+import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
@@ -48,39 +50,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
* be obtained from {@link ExoPlayerFactory}.
*/
@TargetApi(16)
-public class SimpleExoPlayer implements ExoPlayer {
+public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent {
- /**
- * A listener for video rendering information from a {@link SimpleExoPlayer}.
- */
- public interface VideoListener {
-
- /**
- * Called each time there's a change in the size of the video being rendered.
- *
- * @param width The video width in pixels.
- * @param height The video height in pixels.
- * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise
- * rotation in degrees that the application should apply for the video for it to be rendered
- * in the correct orientation. This value will always be zero on API levels 21 and above,
- * since the renderer will apply all necessary rotations internally. On earlier API levels
- * this is not possible. Applications that use {@link android.view.TextureView} can apply
- * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that
- * do not expect to encounter rotated videos can safely ignore this parameter.
- * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
- * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
- * content.
- */
- void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
- float pixelWidthHeightRatio);
-
- /**
- * Called when a frame is rendered for the first time since setting the surface, and when a
- * frame is rendered for the first time since a video track was selected.
- */
- void onRenderedFirstFrame();
-
- }
+ /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */
+ @Deprecated
+ public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {}
private static final String TAG = "SimpleExoPlayer";
@@ -88,13 +62,12 @@ public class SimpleExoPlayer implements ExoPlayer {
private final ExoPlayer player;
private final ComponentListener componentListener;
- private final CopyOnWriteArraySet videoListeners;
+ private final CopyOnWriteArraySet
+ videoListeners;
private final CopyOnWriteArraySet textOutputs;
private final CopyOnWriteArraySet metadataOutputs;
private final CopyOnWriteArraySet videoDebugListeners;
private final CopyOnWriteArraySet audioDebugListeners;
- private final int videoRendererCount;
- private final int audioRendererCount;
private Format videoFormat;
private Format audioFormat;
@@ -111,8 +84,28 @@ public class SimpleExoPlayer implements ExoPlayer {
private AudioAttributes audioAttributes;
private float audioVolume;
- protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector,
- LoadControl loadControl) {
+ /**
+ * @param renderersFactory A factory for creating {@link Renderer}s to 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.
+ */
+ protected SimpleExoPlayer(
+ RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) {
+ this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT);
+ }
+
+ /**
+ * @param renderersFactory A factory for creating {@link Renderer}s to 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.
+ * @param clock The {@link Clock} that will be used by the instance. Should always be {@link
+ * Clock#DEFAULT}, unless the player is being used from a test.
+ */
+ protected SimpleExoPlayer(
+ RenderersFactory renderersFactory,
+ TrackSelector trackSelector,
+ LoadControl loadControl,
+ Clock clock) {
componentListener = new ComponentListener();
videoListeners = new CopyOnWriteArraySet<>();
textOutputs = new CopyOnWriteArraySet<>();
@@ -124,22 +117,6 @@ public class SimpleExoPlayer implements ExoPlayer {
renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener,
componentListener, componentListener);
- // Obtain counts of video and audio renderers.
- int videoRendererCount = 0;
- int audioRendererCount = 0;
- for (Renderer renderer : renderers) {
- switch (renderer.getTrackType()) {
- case C.TRACK_TYPE_VIDEO:
- videoRendererCount++;
- break;
- case C.TRACK_TYPE_AUDIO:
- audioRendererCount++;
- break;
- }
- }
- this.videoRendererCount = videoRendererCount;
- this.audioRendererCount = audioRendererCount;
-
// Set initial values.
audioVolume = 1;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
@@ -147,81 +124,65 @@ public class SimpleExoPlayer implements ExoPlayer {
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects.
- player = createExoPlayerImpl(renderers, trackSelector, loadControl);
+ player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock);
+ }
+
+ @Override
+ public VideoComponent getVideoComponent() {
+ return this;
+ }
+
+ @Override
+ public TextComponent getTextComponent() {
+ return this;
}
/**
* Sets the video scaling mode.
- *
- * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is
- * enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
+ *
+ *
Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer}
+ * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
*
* @param videoScalingMode The video scaling mode.
*/
+ @Override
public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
this.videoScalingMode = videoScalingMode;
- ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
- int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE,
- videoScalingMode);
+ player
+ .createMessage(renderer)
+ .setType(C.MSG_SET_SCALING_MODE)
+ .setPayload(videoScalingMode)
+ .send();
}
}
- player.sendMessages(messages);
}
- /**
- * Returns the video scaling mode.
- */
+ @Override
public @C.VideoScalingMode int getVideoScalingMode() {
return videoScalingMode;
}
- /**
- * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView}
- * currently set on the player.
- */
+ @Override
public void clearVideoSurface() {
setVideoSurface(null);
}
- /**
- * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for
- * tracking the lifecycle of the surface, and must clear the surface by calling
- * {@code setVideoSurface(null)} if the surface is destroyed.
- *
- * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder}
- * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)},
- * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)}
- * rather than this method, since passing the holder allows the player to track the lifecycle of
- * the surface automatically.
- *
- * @param surface The {@link Surface}.
- */
+ @Override
public void setVideoSurface(Surface surface) {
removeSurfaceCallbacks();
setVideoSurfaceInternal(surface, false);
}
- /**
- * Clears the {@link Surface} onto which video is being rendered if it matches the one passed.
- * Else does nothing.
- *
- * @param surface The surface to clear.
- */
+ @Override
public void clearVideoSurface(Surface surface) {
if (surface != null && surface == this.surface) {
setVideoSurface(null);
}
}
- /**
- * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be
- * rendered. The player will track the lifecycle of the surface automatically.
- *
- * @param surfaceHolder The surface holder.
- */
+ @Override
public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
removeSurfaceCallbacks();
this.surfaceHolder = surfaceHolder;
@@ -234,44 +195,24 @@ public class SimpleExoPlayer implements ExoPlayer {
}
}
- /**
- * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being
- * rendered if it matches the one passed. Else does nothing.
- *
- * @param surfaceHolder The surface holder to clear.
- */
+ @Override
public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) {
if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {
setVideoSurfaceHolder(null);
}
}
- /**
- * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the
- * lifecycle of the surface automatically.
- *
- * @param surfaceView The surface view.
- */
+ @Override
public void setVideoSurfaceView(SurfaceView surfaceView) {
setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
}
- /**
- * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed.
- * Else does nothing.
- *
- * @param surfaceView The texture view to clear.
- */
+ @Override
public void clearVideoSurfaceView(SurfaceView surfaceView) {
clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
}
- /**
- * Sets the {@link TextureView} onto which video will be rendered. The player will track the
- * lifecycle of the surface automatically.
- *
- * @param textureView The texture view.
- */
+ @Override
public void setVideoTextureView(TextureView textureView) {
removeSurfaceCallbacks();
this.textureView = textureView;
@@ -288,12 +229,7 @@ public class SimpleExoPlayer implements ExoPlayer {
}
}
- /**
- * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed.
- * Else does nothing.
- *
- * @param textureView The texture view to clear.
- */
+ @Override
public void clearVideoTextureView(TextureView textureView) {
if (textureView != null && textureView == this.textureView) {
setVideoTextureView(null);
@@ -349,15 +285,15 @@ public class SimpleExoPlayer implements ExoPlayer {
*/
public void setAudioAttributes(AudioAttributes audioAttributes) {
this.audioAttributes = audioAttributes;
- ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
- int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES,
- audioAttributes);
+ player
+ .createMessage(renderer)
+ .setType(C.MSG_SET_AUDIO_ATTRIBUTES)
+ .setPayload(audioAttributes)
+ .send();
}
}
- player.sendMessages(messages);
}
/**
@@ -374,14 +310,11 @@ public class SimpleExoPlayer implements ExoPlayer {
*/
public void setVolume(float audioVolume) {
this.audioVolume = audioVolume;
- ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
- int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume);
+ player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send();
}
}
- player.sendMessages(messages);
}
/**
@@ -445,21 +378,13 @@ public class SimpleExoPlayer implements ExoPlayer {
return audioDecoderCounters;
}
- /**
- * Adds a listener to receive video events.
- *
- * @param listener The listener to register.
- */
- public void addVideoListener(VideoListener listener) {
+ @Override
+ public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {
videoListeners.add(listener);
}
- /**
- * Removes a listener of video events.
- *
- * @param listener The listener to unregister.
- */
- public void removeVideoListener(VideoListener listener) {
+ @Override
+ public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) {
videoListeners.remove(listener);
}
@@ -467,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer {
* Sets a listener to receive video events, removing all existing listeners.
*
* @param listener The listener.
- * @deprecated Use {@link #addVideoListener(VideoListener)}.
+ * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
*/
@Deprecated
public void setVideoListener(VideoListener listener) {
@@ -478,30 +403,23 @@ public class SimpleExoPlayer implements ExoPlayer {
}
/**
- * Equivalent to {@link #removeVideoListener(VideoListener)}.
+ * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
*
* @param listener The listener to clear.
- * @deprecated Use {@link #removeVideoListener(VideoListener)}.
+ * @deprecated Use {@link
+ * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}.
*/
@Deprecated
public void clearVideoListener(VideoListener listener) {
removeVideoListener(listener);
}
- /**
- * Registers an output to receive text events.
- *
- * @param listener The output to register.
- */
+ @Override
public void addTextOutput(TextOutput listener) {
textOutputs.add(listener);
}
- /**
- * Removes a text output.
- *
- * @param listener The output to remove.
- */
+ @Override
public void removeTextOutput(TextOutput listener) {
textOutputs.remove(listener);
}
@@ -531,20 +449,10 @@ public class SimpleExoPlayer implements ExoPlayer {
removeTextOutput(output);
}
- /**
- * Registers an output to receive metadata events.
- *
- * @param listener The output to register.
- */
public void addMetadataOutput(MetadataOutput listener) {
metadataOutputs.add(listener);
}
- /**
- * Removes a metadata output.
- *
- * @param listener The output to remove.
- */
public void removeMetadataOutput(MetadataOutput listener) {
metadataOutputs.remove(listener);
}
@@ -735,11 +643,21 @@ public class SimpleExoPlayer implements ExoPlayer {
return player.getPlaybackParameters();
}
+ @Override
+ public void setSeekParameters(@Nullable SeekParameters seekParameters) {
+ player.setSeekParameters(seekParameters);
+ }
+
@Override
public void stop() {
player.stop();
}
+ @Override
+ public void stop(boolean reset) {
+ player.stop(reset);
+ }
+
@Override
public void release() {
player.release();
@@ -757,6 +675,11 @@ public class SimpleExoPlayer implements ExoPlayer {
player.sendMessages(messages);
}
+ @Override
+ public PlayerMessage createMessage(PlayerMessage.Target target) {
+ return player.createMessage(target);
+ }
+
@Override
public void blockingSendMessages(ExoPlayerMessage... messages) {
player.blockingSendMessages(messages);
@@ -870,11 +793,12 @@ public class SimpleExoPlayer implements ExoPlayer {
* @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.
+ * @param clock The {@link Clock} that will be used by this instance.
* @return A new {@link ExoPlayer} instance.
*/
protected ExoPlayer createExoPlayerImpl(
- Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
- return new ExoPlayerImpl(renderers, trackSelector, loadControl);
+ Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) {
+ return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock);
}
private void removeSurfaceCallbacks() {
@@ -895,22 +819,26 @@ public class SimpleExoPlayer implements ExoPlayer {
private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) {
// Note: We don't turn this method into a no-op if the surface is being replaced with itself
// so as to ensure onRenderedFirstFrame callbacks are still called in this case.
- ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
- int count = 0;
+ List messages = new ArrayList<>();
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
- messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface);
+ messages.add(
+ player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send());
}
}
if (this.surface != null && this.surface != surface) {
// We're replacing a surface. Block to ensure that it's not accessed after the method returns.
- player.blockingSendMessages(messages);
+ try {
+ for (PlayerMessage message : messages) {
+ message.blockUntilDelivered();
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
// If we created the previous surface, we are responsible for releasing it.
if (this.ownsSurface) {
this.surface.release();
}
- } else {
- player.sendMessages(messages);
}
this.surface = surface;
this.ownsSurface = ownsSurface;
@@ -957,7 +885,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees,
float pixelWidthHeightRatio) {
- for (VideoListener videoListener : videoListeners) {
+ for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees,
pixelWidthHeightRatio);
}
@@ -970,7 +898,7 @@ public class SimpleExoPlayer implements ExoPlayer {
@Override
public void onRenderedFirstFrame(Surface surface) {
if (SimpleExoPlayer.this.surface == surface) {
- for (VideoListener videoListener : videoListeners) {
+ for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) {
videoListener.onRenderedFirstFrame();
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
index 783278a121..50a3e66880 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import android.util.Pair;
+import com.google.android.exoplayer2.source.ads.AdPlaybackState;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -278,12 +279,7 @@ public abstract class Timeline {
public long durationUs;
private long positionInWindowUs;
- private long[] adGroupTimesUs;
- private int[] adCounts;
- private int[] adsLoadedCounts;
- private int[] adsPlayedCounts;
- private long[][] adDurationsUs;
- private long adResumePositionUs;
+ private AdPlaybackState adPlaybackState;
/**
* Sets the data held by this period.
@@ -300,8 +296,7 @@ public abstract class Timeline {
*/
public Period set(Object id, Object uid, int windowIndex, long durationUs,
long positionInWindowUs) {
- return set(id, uid, windowIndex, durationUs, positionInWindowUs, null, null, null, null,
- null, C.TIME_UNSET);
+ return set(id, uid, windowIndex, durationUs, positionInWindowUs, AdPlaybackState.NONE);
}
/**
@@ -315,33 +310,23 @@ public abstract class Timeline {
* @param positionInWindowUs The position of the start of this period relative to the start of
* the window to which it belongs, in milliseconds. May be negative if the start of the
* period is not within the window.
- * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
- * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
- * the period has a postroll ad.
- * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
- * if the number of ads is not yet known.
- * @param adsLoadedCounts The number of ads loaded so far in each ad group.
- * @param adsPlayedCounts The number of ads played so far in each ad group.
- * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
- * may be {@link C#TIME_UNSET} if the duration is not yet known.
- * @param adResumePositionUs The position offset in the first unplayed ad at which to begin
- * playback, in microseconds.
+ * @param adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if
+ * there are no ads.
* @return This period, for convenience.
*/
- public Period set(Object id, Object uid, int windowIndex, long durationUs,
- long positionInWindowUs, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
- int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) {
+ public Period set(
+ Object id,
+ Object uid,
+ int windowIndex,
+ long durationUs,
+ long positionInWindowUs,
+ AdPlaybackState adPlaybackState) {
this.id = id;
this.uid = uid;
this.windowIndex = windowIndex;
this.durationUs = durationUs;
this.positionInWindowUs = positionInWindowUs;
- this.adGroupTimesUs = adGroupTimesUs;
- this.adCounts = adCounts;
- this.adsLoadedCounts = adsLoadedCounts;
- this.adsPlayedCounts = adsPlayedCounts;
- this.adDurationsUs = adDurationsUs;
- this.adResumePositionUs = adResumePositionUs;
+ this.adPlaybackState = adPlaybackState;
return this;
}
@@ -381,7 +366,7 @@ public abstract class Timeline {
* Returns the number of ad groups in the period.
*/
public int getAdGroupCount() {
- return adGroupTimesUs == null ? 0 : adGroupTimesUs.length;
+ return adPlaybackState.adGroupCount;
}
/**
@@ -392,17 +377,33 @@ public abstract class Timeline {
* @return The time of the ad group at the index, in microseconds.
*/
public long getAdGroupTimeUs(int adGroupIndex) {
- return adGroupTimesUs[adGroupIndex];
+ return adPlaybackState.adGroupTimesUs[adGroupIndex];
}
/**
- * Returns the number of ads that have been played in the specified ad group in the period.
+ * Returns the index of the first ad in the specified ad group that should be played, or the
+ * number of ads in the ad group if no ads should be played.
*
* @param adGroupIndex The ad group index.
- * @return The number of ads that have been played.
+ * @return The index of the first ad that should be played, or the number of ads in the ad group
+ * if no ads should be played.
*/
- public int getPlayedAdCount(int adGroupIndex) {
- return adsPlayedCounts[adGroupIndex];
+ public int getFirstAdIndexToPlay(int adGroupIndex) {
+ return adPlaybackState.adGroups[adGroupIndex].getFirstAdIndexToPlay();
+ }
+
+ /**
+ * Returns the index of the next ad in the specified ad group that should be played after
+ * playing {@code adIndexInAdGroup}, or the number of ads in the ad group if no later ads should
+ * be played.
+ *
+ * @param adGroupIndex The ad group index.
+ * @param lastPlayedAdIndex The last played ad index in the ad group.
+ * @return The index of the next ad that should be played, or the number of ads in the ad group
+ * if the ad group does not have any ads remaining to play.
+ */
+ public int getNextAdIndexToPlay(int adGroupIndex, int lastPlayedAdIndex) {
+ return adPlaybackState.adGroups[adGroupIndex].getNextAdIndexToPlay(lastPlayedAdIndex);
}
/**
@@ -412,51 +413,30 @@ public abstract class Timeline {
* @return Whether the ad group at index {@code adGroupIndex} has been played.
*/
public boolean hasPlayedAdGroup(int adGroupIndex) {
- return adCounts[adGroupIndex] != C.INDEX_UNSET
- && adsPlayedCounts[adGroupIndex] == adCounts[adGroupIndex];
+ return !adPlaybackState.adGroups[adGroupIndex].hasUnplayedAds();
}
/**
* Returns the index of the ad group at or before {@code positionUs}, if that ad group is
- * unplayed. Returns {@link C#INDEX_UNSET} if the ad group before {@code positionUs} has been
- * played, or if there is no such ad group.
+ * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has
+ * no ads remaining to be played, or if there is no such ad group.
*
* @param positionUs The position at or before which to find an ad group, in microseconds.
* @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/
public int getAdGroupIndexForPositionUs(long positionUs) {
- if (adGroupTimesUs == null) {
- return C.INDEX_UNSET;
- }
- // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
- // In practice we expect there to be few ad groups so the search shouldn't be expensive.
- int index = adGroupTimesUs.length - 1;
- while (index >= 0 && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE
- || adGroupTimesUs[index] > positionUs)) {
- index--;
- }
- return index >= 0 && !hasPlayedAdGroup(index) ? index : C.INDEX_UNSET;
+ return adPlaybackState.getAdGroupIndexForPositionUs(positionUs);
}
/**
- * Returns the index of the next unplayed ad group after {@code positionUs}. Returns
- * {@link C#INDEX_UNSET} if there is no such ad group.
+ * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
+ * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
*
* @param positionUs The position after which to find an ad group, in microseconds.
* @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/
public int getAdGroupIndexAfterPositionUs(long positionUs) {
- if (adGroupTimesUs == null) {
- return C.INDEX_UNSET;
- }
- // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
- // In practice we expect there to be few ad groups so the search shouldn't be expensive.
- int index = 0;
- while (index < adGroupTimesUs.length && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
- && (positionUs >= adGroupTimesUs[index] || hasPlayedAdGroup(index))) {
- index++;
- }
- return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
+ return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs);
}
/**
@@ -467,7 +447,7 @@ public abstract class Timeline {
* @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known.
*/
public int getAdCountInAdGroup(int adGroupIndex) {
- return adCounts[adGroupIndex];
+ return adPlaybackState.adGroups[adGroupIndex].count;
}
/**
@@ -478,7 +458,9 @@ public abstract class Timeline {
* @return Whether the URL for the specified ad is known.
*/
public boolean isAdAvailable(int adGroupIndex, int adIndexInAdGroup) {
- return adIndexInAdGroup < adsLoadedCounts[adGroupIndex];
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ return adGroup.count != C.LENGTH_UNSET
+ && adGroup.states[adIndexInAdGroup] != AdPlaybackState.AD_STATE_UNAVAILABLE;
}
/**
@@ -490,10 +472,8 @@ public abstract class Timeline {
* @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known.
*/
public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) {
- if (adIndexInAdGroup >= adDurationsUs[adGroupIndex].length) {
- return C.TIME_UNSET;
- }
- return adDurationsUs[adGroupIndex][adIndexInAdGroup];
+ AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex];
+ return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET;
}
/**
@@ -501,7 +481,7 @@ public abstract class Timeline {
* microseconds.
*/
public long getAdResumePositionUs() {
- return adResumePositionUs;
+ return adPlaybackState.adResumePositionUs;
}
}
@@ -710,18 +690,6 @@ public abstract class Timeline {
== C.INDEX_UNSET;
}
- /**
- * Populates a {@link Period} with data for the period at the specified index. Does not populate
- * {@link Period#id} and {@link Period#uid}.
- *
- * @param periodIndex The index of the period.
- * @param period The {@link Period} to populate. Must not be null.
- * @return The populated {@link Period}, for convenience.
- */
- public final Period getPeriod(int periodIndex, Period period) {
- return getPeriod(periodIndex, period, false);
- }
-
/**
* Calls {@link #getPeriodPosition(Window, Period, int, long, long)} with a zero default position
* projection.
@@ -766,6 +734,18 @@ public abstract class Timeline {
return Pair.create(periodIndex, periodPositionUs);
}
+ /**
+ * Populates a {@link Period} with data for the period at the specified index. Does not populate
+ * {@link Period#id} and {@link Period#uid}.
+ *
+ * @param periodIndex The index of the period.
+ * @param period The {@link Period} to populate. Must not be null.
+ * @return The populated {@link Period}, for convenience.
+ */
+ public final Period getPeriod(int periodIndex, Period period) {
+ return getPeriod(periodIndex, period, false);
+ }
+
/**
* Populates a {@link Period} with data for the period at the specified index.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
index e9ffab7ace..f45a6a11c6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java
@@ -27,9 +27,7 @@ import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
-/**
- * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams.
- */
+/** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */
public final class Ac3Util {
/**
@@ -93,6 +91,17 @@ public final class Ac3Util {
}
+ /**
+ * The number of samples to store in each output chunk when rechunking TrueHD streams. The number
+ * of samples extracted from the container corresponding to one syncframe must be an integer
+ * multiple of this value.
+ */
+ public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 8;
+ /**
+ * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count.
+ */
+ public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12;
+
/**
* The number of new samples per (E-)AC-3 audio block.
*/
@@ -189,7 +198,7 @@ public final class Ac3Util {
if (data.bytesLeft() > 0) {
nextByte = data.readUnsignedByte();
if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a
- mimeType = MimeTypes.AUDIO_ATMOS;
+ mimeType = MimeTypes.AUDIO_E_AC3_JOC;
}
}
return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE,
@@ -376,7 +385,7 @@ public final class Ac3Util {
if (data.readBit()) { // addbsie
int addbsil = data.readBits(6);
if (addbsil == 1 && data.readBits(8) == 1) { // addbsi
- mimeType = MimeTypes.AUDIO_ATMOS;
+ mimeType = MimeTypes.AUDIO_E_AC3_JOC;
}
}
} else /* is AC-3 */ {
@@ -441,6 +450,43 @@ public final class Ac3Util {
: BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]);
}
+ /**
+ * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the
+ * buffer is not the start of a syncframe.
+ *
+ * @param syncframe The bytes from which to read the syncframe. Must be at least {@link
+ * #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long.
+ * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't
+ * contain the start of a syncframe.
+ */
+ public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) {
+ // TODO: Link to specification if available.
+ if (syncframe[4] != (byte) 0xF8
+ || syncframe[5] != (byte) 0x72
+ || syncframe[6] != (byte) 0x6F
+ || syncframe[7] != (byte) 0xBA) {
+ return 0;
+ }
+ return 40 << (syncframe[8] & 7);
+ }
+
+ /**
+ * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer
+ * is not the start of a syncframe. The buffer's position is not modified.
+ *
+ * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least
+ * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining.
+ * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the
+ * start of a syncframe.
+ */
+ public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) {
+ // TODO: Link to specification if available.
+ if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) {
+ return 0;
+ }
+ return 40 << (buffer.get(buffer.position() + 8) & 0x07);
+ }
+
private static int getAc3SyncframeSize(int fscod, int frmsizecod) {
int halfFrmsizecod = frmsizecod / 2;
if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java
index b5ee052924..ac4f632d62 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java
@@ -15,27 +15,21 @@
*/
package com.google.android.exoplayer2.audio;
-/**
- * Thrown when an audio decoder error occurs.
- */
-public abstract class AudioDecoderException extends Exception {
+/** Thrown when an audio decoder error occurs. */
+public class AudioDecoderException extends Exception {
- /**
- * @param detailMessage The detail message for this exception.
- */
- public AudioDecoderException(String detailMessage) {
- super(detailMessage);
+ /** @param message The detail message for this exception. */
+ public AudioDecoderException(String message) {
+ super(message);
}
/**
- * @param detailMessage The detail message for this exception.
- * @param cause the cause (which is saved for later retrieval by the
- * {@link #getCause()} method). (A null value is
- * permitted, and indicates that the cause is nonexistent or
- * unknown.)
+ * @param message The detail message for this exception.
+ * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
+ * A null value is permitted, and indicates that the cause is nonexistent or unknown.
*/
- public AudioDecoderException(String detailMessage, Throwable cause) {
- super(detailMessage, cause);
+ public AudioDecoderException(String message, Throwable cause) {
+ super(message, cause);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
index 17b90680dd..50b484b938 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java
@@ -51,6 +51,8 @@ import java.util.Arrays;
/**
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map.
+ *
+ * @see AudioSink#configure(int, int, int, int, int[], int, int)
*/
public void setChannelMap(int[] outputChannels) {
pendingOutputChannels = outputChannels;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
index eb27c0fe55..bb9135edbf 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java
@@ -55,11 +55,9 @@ public final class DefaultAudioSink implements AudioSink {
*/
public static final class InvalidAudioTrackTimestampException extends RuntimeException {
- /**
- * @param detailMessage The detail message for this exception.
- */
- public InvalidAudioTrackTimestampException(String detailMessage) {
- super(detailMessage);
+ /** @param message The detail message for this exception. */
+ public InvalidAudioTrackTimestampException(String message) {
+ super(message);
}
}
@@ -166,10 +164,12 @@ public final class DefaultAudioSink implements AudioSink {
public static boolean failOnSpuriousAudioTimestamp = false;
@Nullable private final AudioCapabilities audioCapabilities;
+ private final boolean enableConvertHighResIntPcmToFloat;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final TrimmingAudioProcessor trimmingAudioProcessor;
private final SonicAudioProcessor sonicAudioProcessor;
- private final AudioProcessor[] availableAudioProcessors;
+ private final AudioProcessor[] toIntPcmAvailableAudioProcessors;
+ private final AudioProcessor[] toFloatPcmAvailableAudioProcessors;
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
private final AudioTrackUtil audioTrackUtil;
@@ -182,12 +182,14 @@ public final class DefaultAudioSink implements AudioSink {
private AudioTrack keepSessionIdAudioTrack;
private AudioTrack audioTrack;
private boolean isInputPcm;
+ private boolean shouldConvertHighResIntPcmToFloat;
private int inputSampleRate;
private int sampleRate;
private int channelConfig;
private @C.Encoding int outputEncoding;
private AudioAttributes audioAttributes;
private boolean processingEnabled;
+ private boolean canApplyPlaybackParameters;
private int bufferSize;
private long bufferSizeUs;
@@ -243,7 +245,25 @@ public final class DefaultAudioSink implements AudioSink {
*/
public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities,
AudioProcessor[] audioProcessors) {
+ this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false);
+ }
+
+ /**
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
+ * output. May be empty.
+ * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution
+ * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer
+ * audio processing (for example, speed and pitch adjustment) will not be available when float
+ * output is in use.
+ */
+ public DefaultAudioSink(
+ @Nullable AudioCapabilities audioCapabilities,
+ AudioProcessor[] audioProcessors,
+ boolean enableConvertHighResIntPcmToFloat) {
this.audioCapabilities = audioCapabilities;
+ this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat;
releasingConditionVariable = new ConditionVariable(true);
if (Util.SDK_INT >= 18) {
try {
@@ -261,12 +281,14 @@ public final class DefaultAudioSink implements AudioSink {
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
sonicAudioProcessor = new SonicAudioProcessor();
- availableAudioProcessors = new AudioProcessor[4 + audioProcessors.length];
- availableAudioProcessors[0] = new ResamplingAudioProcessor();
- availableAudioProcessors[1] = channelMappingAudioProcessor;
- availableAudioProcessors[2] = trimmingAudioProcessor;
- System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length);
- availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor;
+ toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length];
+ toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor();
+ toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor;
+ toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor;
+ System.arraycopy(
+ audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length);
+ toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor;
+ toFloatPcmAvailableAudioProcessors = new AudioProcessor[] {new FloatResamplingAudioProcessor()};
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
@@ -344,15 +366,20 @@ public final class DefaultAudioSink implements AudioSink {
int channelCount = inputChannelCount;
int sampleRate = inputSampleRate;
isInputPcm = isEncodingPcm(inputEncoding);
+ shouldConvertHighResIntPcmToFloat =
+ enableConvertHighResIntPcmToFloat
+ && isEncodingSupported(C.ENCODING_PCM_32BIT)
+ && Util.isEncodingHighResolutionIntegerPcm(inputEncoding);
if (isInputPcm) {
pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount);
}
@C.Encoding int encoding = inputEncoding;
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT;
+ canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat;
if (processingEnabled) {
trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples);
channelMappingAudioProcessor.setChannelMap(outputChannels);
- for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) {
try {
flush |= audioProcessor.configure(sampleRate, channelCount, encoding);
} catch (AudioProcessor.UnhandledFormatException e) {
@@ -448,9 +475,12 @@ public final class DefaultAudioSink implements AudioSink {
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
// AC-3 allows bitrates up to 640 kbit/s.
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
- } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ {
+ } else if (outputEncoding == C.ENCODING_DTS) {
// DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
+ } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ {
+ // HD passthrough requires a larger buffer to avoid underrun.
+ bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND);
}
}
bufferSizeUs =
@@ -459,7 +489,7 @@ public final class DefaultAudioSink implements AudioSink {
private void resetAudioProcessors() {
ArrayList newAudioProcessors = new ArrayList<>();
- for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ for (AudioProcessor audioProcessor : getAvailableAudioProcessors()) {
if (audioProcessor.isActive()) {
newAudioProcessors.add(audioProcessor);
} else {
@@ -582,6 +612,13 @@ public final class DefaultAudioSink implements AudioSink {
if (!isInputPcm && framesPerEncodedSample == 0) {
// If this is the first encoded sample, calculate the sample size in frames.
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
+ if (framesPerEncodedSample == 0) {
+ // We still don't know the number of frames per sample, so drop the buffer.
+ // For TrueHD this can occur after some seek operations, as not every sample starts with
+ // a syncframe header. If we chunked samples together so the extracted samples always
+ // started with a syncframe header, the chunks would be too large.
+ return true;
+ }
}
if (drainingPlaybackParameters != null) {
@@ -800,8 +837,7 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) {
- if (isInitialized() && !processingEnabled) {
- // The playback parameters are always the default if processing is disabled.
+ if (isInitialized() && !canApplyPlaybackParameters) {
this.playbackParameters = PlaybackParameters.DEFAULT;
return this.playbackParameters;
}
@@ -956,7 +992,10 @@ public final class DefaultAudioSink implements AudioSink {
public void release() {
reset();
releaseKeepSessionIdAudioTrack();
- for (AudioProcessor audioProcessor : availableAudioProcessors) {
+ for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) {
+ audioProcessor.reset();
+ }
+ for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) {
audioProcessor.reset();
}
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
@@ -1012,7 +1051,8 @@ public final class DefaultAudioSink implements AudioSink {
}
// We are playing data at a previous playback speed, so fall back to multiplying by the speed.
return playbackParametersOffsetUs
- + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs));
+ + Util.getMediaDurationForPlayoutDuration(
+ positionUs - playbackParametersPositionUs, playbackParameters.speed);
}
/**
@@ -1213,6 +1253,12 @@ public final class DefaultAudioSink implements AudioSink {
MODE_STATIC, audioSessionId);
}
+ private AudioProcessor[] getAvailableAudioProcessors() {
+ return shouldConvertHighResIntPcmToFloat
+ ? toFloatPcmAvailableAudioProcessors
+ : toIntPcmAvailableAudioProcessors;
+ }
+
private static boolean isEncodingPcm(@C.Encoding int encoding) {
return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT
|| encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT
@@ -1226,6 +1272,9 @@ public final class DefaultAudioSink implements AudioSink {
return Ac3Util.getAc3SyncframeAudioSampleCount();
} else if (encoding == C.ENCODING_E_AC3) {
return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer);
+ } else if (encoding == C.ENCODING_DOLBY_TRUEHD) {
+ return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer)
+ * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT;
} else {
throw new IllegalStateException("Unexpected audio encoding: " + encoding);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
index 9e9b927fab..dc07b1a646 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DtsUtil.java
@@ -20,12 +20,22 @@ import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import java.nio.ByteBuffer;
+import java.util.Arrays;
/**
* Utility methods for parsing DTS frames.
*/
public final class DtsUtil {
+ private static final int SYNC_VALUE_BE = 0x7FFE8001;
+ private static final int SYNC_VALUE_14B_BE = 0x1FFFE800;
+ private static final int SYNC_VALUE_LE = 0xFE7F0180;
+ private static final int SYNC_VALUE_14B_LE = 0xFF1F00E8;
+ private static final byte FIRST_BYTE_BE = (byte) (SYNC_VALUE_BE >>> 24);
+ private static final byte FIRST_BYTE_14B_BE = (byte) (SYNC_VALUE_14B_BE >>> 24);
+ private static final byte FIRST_BYTE_LE = (byte) (SYNC_VALUE_LE >>> 24);
+ private static final byte FIRST_BYTE_14B_LE = (byte) (SYNC_VALUE_14B_LE >>> 24);
+
/**
* Maps AMODE to the number of channels. See ETSI TS 102 114 table 5.4.
*/
@@ -45,6 +55,20 @@ public final class DtsUtil {
384, 448, 512, 640, 768, 896, 1024, 1152, 1280, 1536, 1920, 2048, 2304, 2560, 2688, 2816,
2823, 2944, 3072, 3840, 4096, 6144, 7680};
+ /**
+ * Returns whether a given integer matches a DTS sync word. Synchronization and storage modes are
+ * defined in ETSI TS 102 114 V1.1.1 (2002-08), Section 5.3.
+ *
+ * @param word An integer.
+ * @return Whether a given integer matches a DTS sync word.
+ */
+ public static boolean isSyncWord(int word) {
+ return word == SYNC_VALUE_BE
+ || word == SYNC_VALUE_LE
+ || word == SYNC_VALUE_14B_BE
+ || word == SYNC_VALUE_14B_LE;
+ }
+
/**
* Returns the DTS format given {@code data} containing the DTS frame according to ETSI TS 102 114
* subsections 5.3/5.4.
@@ -57,8 +81,8 @@ public final class DtsUtil {
*/
public static Format parseDtsFormat(byte[] frame, String trackId, String language,
DrmInitData drmInitData) {
- ParsableBitArray frameBits = new ParsableBitArray(frame);
- frameBits.skipBits(4 * 8 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
+ ParsableBitArray frameBits = getNormalizedFrameHeader(frame);
+ frameBits.skipBits(32 + 1 + 5 + 1 + 7 + 14); // SYNC, FTYPE, SHORT, CPF, NBLKS, FSIZE
int amode = frameBits.readBits(6);
int channelCount = CHANNELS_BY_AMODE[amode];
int sfreq = frameBits.readBits(4);
@@ -79,8 +103,21 @@ public final class DtsUtil {
* @return The number of audio samples represented by the frame.
*/
public static int parseDtsAudioSampleCount(byte[] data) {
- // See ETSI TS 102 114 subsection 5.4.1.
- int nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2);
+ int nblks;
+ switch (data[0]) {
+ case FIRST_BYTE_LE:
+ nblks = ((data[5] & 0x01) << 6) | ((data[4] & 0xFC) >> 2);
+ break;
+ case FIRST_BYTE_14B_LE:
+ nblks = ((data[4] & 0x07) << 4) | ((data[7] & 0x3C) >> 2);
+ break;
+ case FIRST_BYTE_14B_BE:
+ nblks = ((data[5] & 0x07) << 4) | ((data[6] & 0x3C) >> 2);
+ break;
+ default:
+ // We blindly assume FIRST_BYTE_BE if none of the others match.
+ nblks = ((data[4] & 0x01) << 6) | ((data[5] & 0xFC) >> 2);
+ }
return (nblks + 1) * 32;
}
@@ -94,8 +131,21 @@ public final class DtsUtil {
public static int parseDtsAudioSampleCount(ByteBuffer buffer) {
// See ETSI TS 102 114 subsection 5.4.1.
int position = buffer.position();
- int nblks = ((buffer.get(position + 4) & 0x01) << 6)
- | ((buffer.get(position + 5) & 0xFC) >> 2);
+ int nblks;
+ switch (buffer.get(position)) {
+ case FIRST_BYTE_LE:
+ nblks = ((buffer.get(position + 5) & 0x01) << 6) | ((buffer.get(position + 4) & 0xFC) >> 2);
+ break;
+ case FIRST_BYTE_14B_LE:
+ nblks = ((buffer.get(position + 4) & 0x07) << 4) | ((buffer.get(position + 7) & 0x3C) >> 2);
+ break;
+ case FIRST_BYTE_14B_BE:
+ nblks = ((buffer.get(position + 5) & 0x07) << 4) | ((buffer.get(position + 6) & 0x3C) >> 2);
+ break;
+ default:
+ // We blindly assume FIRST_BYTE_BE if none of the others match.
+ nblks = ((buffer.get(position + 4) & 0x01) << 6) | ((buffer.get(position + 5) & 0xFC) >> 2);
+ }
return (nblks + 1) * 32;
}
@@ -106,9 +156,59 @@ public final class DtsUtil {
* @return The frame's size in bytes.
*/
public static int getDtsFrameSize(byte[] data) {
- return (((data[5] & 0x02) << 12)
- | ((data[6] & 0xFF) << 4)
- | ((data[7] & 0xF0) >> 4)) + 1;
+ int fsize;
+ boolean uses14BitPerWord = false;
+ switch (data[0]) {
+ case FIRST_BYTE_14B_BE:
+ fsize = (((data[6] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[8] & 0x3C) >> 2)) + 1;
+ uses14BitPerWord = true;
+ break;
+ case FIRST_BYTE_LE:
+ fsize = (((data[4] & 0x03) << 12) | ((data[7] & 0xFF) << 4) | ((data[6] & 0xF0) >> 4)) + 1;
+ break;
+ case FIRST_BYTE_14B_LE:
+ fsize = (((data[7] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[9] & 0x3C) >> 2)) + 1;
+ uses14BitPerWord = true;
+ break;
+ default:
+ // We blindly assume FIRST_BYTE_BE if none of the others match.
+ fsize = (((data[5] & 0x03) << 12) | ((data[6] & 0xFF) << 4) | ((data[7] & 0xF0) >> 4)) + 1;
+ }
+
+ // If the frame is stored in 14-bit mode, adjust the frame size to reflect the actual byte size.
+ return uses14BitPerWord ? fsize * 16 / 14 : fsize;
+ }
+
+ private static ParsableBitArray getNormalizedFrameHeader(byte[] frameHeader) {
+ if (frameHeader[0] == FIRST_BYTE_BE) {
+ // The frame is already 16-bit mode, big endian.
+ return new ParsableBitArray(frameHeader);
+ }
+ // Data is not normalized, but we don't want to modify frameHeader.
+ frameHeader = Arrays.copyOf(frameHeader, frameHeader.length);
+ if (isLittleEndianFrameHeader(frameHeader)) {
+ // Change endianness.
+ for (int i = 0; i < frameHeader.length - 1; i += 2) {
+ byte temp = frameHeader[i];
+ frameHeader[i] = frameHeader[i + 1];
+ frameHeader[i + 1] = temp;
+ }
+ }
+ ParsableBitArray frameBits = new ParsableBitArray(frameHeader);
+ if (frameHeader[0] == (byte) (SYNC_VALUE_14B_BE >> 24)) {
+ // Discard the 2 most significant bits of each 16 bit word.
+ ParsableBitArray scratchBits = new ParsableBitArray(frameHeader);
+ while (scratchBits.bitsLeft() >= 16) {
+ scratchBits.skipBits(2);
+ frameBits.putInt(scratchBits.readBits(14), 14);
+ }
+ }
+ frameBits.reset(frameHeader);
+ return frameBits;
+ }
+
+ private static boolean isLittleEndianFrameHeader(byte[] frameHeader) {
+ return frameHeader[0] == FIRST_BYTE_LE || frameHeader[0] == FIRST_BYTE_14B_LE;
}
private DtsUtil() {}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
new file mode 100644
index 0000000000..215b04821b
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2018 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.audio;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM
+ * audio.
+ */
+/* package */ final class FloatResamplingAudioProcessor implements AudioProcessor {
+
+ private static final int FLOAT_NAN_AS_INT = Float.floatToIntBits(Float.NaN);
+ private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF;
+
+ private int sampleRateHz;
+ private int channelCount;
+ private @C.PcmEncoding int sourceEncoding;
+ private ByteBuffer buffer;
+ private ByteBuffer outputBuffer;
+ private boolean inputEnded;
+
+ /** Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_FLOAT}. */
+ public FloatResamplingAudioProcessor() {
+ sampleRateHz = Format.NO_VALUE;
+ channelCount = Format.NO_VALUE;
+ sourceEncoding = C.ENCODING_INVALID;
+ buffer = EMPTY_BUFFER;
+ outputBuffer = EMPTY_BUFFER;
+ }
+
+ @Override
+ public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
+ throws UnhandledFormatException {
+ if (!Util.isEncodingHighResolutionIntegerPcm(encoding)) {
+ throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
+ }
+ if (this.sampleRateHz == sampleRateHz
+ && this.channelCount == channelCount
+ && sourceEncoding == encoding) {
+ return false;
+ }
+ this.sampleRateHz = sampleRateHz;
+ this.channelCount = channelCount;
+ sourceEncoding = encoding;
+ return true;
+ }
+
+ @Override
+ public boolean isActive() {
+ return Util.isEncodingHighResolutionIntegerPcm(sourceEncoding);
+ }
+
+ @Override
+ public int getOutputChannelCount() {
+ return channelCount;
+ }
+
+ @Override
+ public int getOutputEncoding() {
+ return C.ENCODING_PCM_FLOAT;
+ }
+
+ @Override
+ public int getOutputSampleRateHz() {
+ return sampleRateHz;
+ }
+
+ @Override
+ public void queueInput(ByteBuffer inputBuffer) {
+ Assertions.checkState(isActive());
+
+ boolean isInput32Bit = sourceEncoding == C.ENCODING_PCM_32BIT;
+ int position = inputBuffer.position();
+ int limit = inputBuffer.limit();
+ int size = limit - position;
+
+ int resampledSize = isInput32Bit ? size : (size / 3) * 4;
+ if (buffer.capacity() < resampledSize) {
+ buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
+ } else {
+ buffer.clear();
+ }
+ if (isInput32Bit) {
+ for (int i = position; i < limit; i += 4) {
+ int pcm32BitInteger =
+ (inputBuffer.get(i) & 0xFF)
+ | ((inputBuffer.get(i + 1) & 0xFF) << 8)
+ | ((inputBuffer.get(i + 2) & 0xFF) << 16)
+ | ((inputBuffer.get(i + 3) & 0xFF) << 24);
+ writePcm32BitFloat(pcm32BitInteger, buffer);
+ }
+ } else {
+ for (int i = position; i < limit; i += 3) {
+ int pcm32BitInteger =
+ ((inputBuffer.get(i) & 0xFF) << 8)
+ | ((inputBuffer.get(i + 1) & 0xFF) << 16)
+ | ((inputBuffer.get(i + 2) & 0xFF) << 24);
+ writePcm32BitFloat(pcm32BitInteger, buffer);
+ }
+ }
+
+ inputBuffer.position(inputBuffer.limit());
+ buffer.flip();
+ outputBuffer = buffer;
+ }
+
+ @Override
+ public void queueEndOfStream() {
+ inputEnded = true;
+ }
+
+ @Override
+ public ByteBuffer getOutput() {
+ ByteBuffer outputBuffer = this.outputBuffer;
+ this.outputBuffer = EMPTY_BUFFER;
+ return outputBuffer;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ @Override
+ public boolean isEnded() {
+ return inputEnded && outputBuffer == EMPTY_BUFFER;
+ }
+
+ @Override
+ public void flush() {
+ outputBuffer = EMPTY_BUFFER;
+ inputEnded = false;
+ }
+
+ @Override
+ public void reset() {
+ flush();
+ buffer = EMPTY_BUFFER;
+ sampleRateHz = Format.NO_VALUE;
+ channelCount = Format.NO_VALUE;
+ sourceEncoding = C.ENCODING_INVALID;
+ }
+
+ /**
+ * Converts the provided 32-bit integer to a 32-bit float value and writes it to {@code buffer}.
+ *
+ * @param pcm32BitInt The 32-bit integer value to convert to 32-bit float in [-1.0, 1.0].
+ * @param buffer The output buffer.
+ */
+ private static void writePcm32BitFloat(int pcm32BitInt, ByteBuffer buffer) {
+ float pcm32BitFloat = (float) (PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR * pcm32BitInt);
+ int floatBits = Float.floatToIntBits(pcm32BitFloat);
+ if (floatBits == FLOAT_NAN_AS_INT) {
+ floatBits = Float.floatToIntBits((float) 0.0);
+ }
+ buffer.putInt(floatBits);
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
index 25ad847f7e..33a67554a5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java
@@ -24,9 +24,12 @@ import android.os.Handler;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
@@ -41,6 +44,17 @@ import java.nio.ByteBuffer;
/**
* Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}.
+ *
+ * This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ *
+ * - Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be
+ * a {@link Float} with 0 being silence and 1 being unity gain.
+ *
- Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The
+ * message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes}
+ * instance that will configure the underlying audio track.
+ *
*/
@TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
@@ -57,6 +71,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private int encoderDelay;
private int encoderPadding;
private long currentPositionUs;
+ private boolean allowFirstBufferPositionDiscontinuity;
private boolean allowPositionDiscontinuity;
/**
@@ -240,14 +255,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
+ MediaFormat mediaFormat = getMediaFormatForPlayback(format);
if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
- passthroughMediaFormat = format.getFrameworkMediaFormatV16();
+ passthroughMediaFormat = mediaFormat;
passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW);
codec.configure(passthroughMediaFormat, null, crypto, 0);
passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType);
} else {
- codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0);
+ codec.configure(mediaFormat, null, crypto, 0);
passthroughMediaFormat = null;
}
}
@@ -352,6 +368,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
super.onPositionReset(positionUs, joining);
audioSink.reset();
currentPositionUs = positionUs;
+ allowFirstBufferPositionDiscontinuity = true;
allowPositionDiscontinuity = true;
}
@@ -364,6 +381,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onStopped() {
audioSink.pause();
+ updateCurrentPosition();
super.onStopped();
}
@@ -393,11 +411,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
public long getPositionUs() {
- long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
- if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
- currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
- : Math.max(currentPositionUs, newCurrentPositionUs);
- allowPositionDiscontinuity = false;
+ if (getState() == STATE_STARTED) {
+ updateCurrentPosition();
}
return currentPositionUs;
}
@@ -412,6 +427,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return audioSink.getPlaybackParameters();
}
+ @Override
+ protected void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) {
+ // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314].
+ // Allow the position to jump if the first presentable input buffer has a timestamp that
+ // differs significantly from what was expected.
+ if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) {
+ currentPositionUs = buffer.timeUs;
+ }
+ allowFirstBufferPositionDiscontinuity = false;
+ }
+ }
+
@Override
protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs,
@@ -466,6 +494,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
+ private void updateCurrentPosition() {
+ long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
+ if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
+ currentPositionUs =
+ allowPositionDiscontinuity
+ ? newCurrentPositionUs
+ : Math.max(currentPositionUs, newCurrentPositionUs);
+ allowPositionDiscontinuity = false;
+ }
+ }
+
/**
* Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
index a78adbcee3..01123f3c59 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java
@@ -21,7 +21,8 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
- * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}.
+ * An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit
+ * integer PCM audio.
*/
/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
@@ -102,6 +103,7 @@ import java.nio.ByteOrder;
resampledSize = size / 2;
break;
case C.ENCODING_PCM_16BIT:
+ case C.ENCODING_PCM_FLOAT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
index d9ad549104..83c33ee6d7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -23,9 +23,11 @@ import android.support.annotation.IntDef;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
+import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.PlayerMessage.Target;
import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
@@ -45,6 +47,17 @@ import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders audio using a {@link SimpleDecoder}.
+ *
+ * This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ *
+ * - Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be
+ * a {@link Float} with 0 being silence and 1 being unity gain.
+ *
- Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The
+ * message payload should be an {@link com.google.android.exoplayer2.audio.AudioAttributes}
+ * instance that will configure the underlying audio track.
+ *
*/
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
@@ -92,6 +105,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private boolean audioTrackNeedsConfigure;
private long currentPositionUs;
+ private boolean allowFirstBufferPositionDiscontinuity;
private boolean allowPositionDiscontinuity;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
@@ -403,6 +417,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return false;
}
inputBuffer.flip();
+ onQueueInputBuffer(inputBuffer);
decoder.queueInputBuffer(inputBuffer);
decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++;
@@ -426,7 +441,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
try {
audioSink.playToEndOfStream();
} catch (AudioSink.WriteException e) {
- throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
+ throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
@@ -459,11 +474,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public long getPositionUs() {
- long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
- if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
- currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs
- : Math.max(currentPositionUs, newCurrentPositionUs);
- allowPositionDiscontinuity = false;
+ if (getState() == STATE_STARTED) {
+ updateCurrentPosition();
}
return currentPositionUs;
}
@@ -494,6 +506,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
audioSink.reset();
currentPositionUs = positionUs;
+ allowFirstBufferPositionDiscontinuity = true;
allowPositionDiscontinuity = true;
inputStreamEnded = false;
outputStreamEnded = false;
@@ -510,6 +523,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onStopped() {
audioSink.pause();
+ updateCurrentPosition();
}
@Override
@@ -540,6 +554,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
}
+ @Override
+ public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+ switch (messageType) {
+ case C.MSG_SET_VOLUME:
+ audioSink.setVolume((Float) message);
+ break;
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioSink.setAudioAttributes(audioAttributes);
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ break;
+ }
+ }
+
private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) {
return;
@@ -552,10 +582,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
- throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ // Continue for now. We may be able to avoid failure if the session recovers, or if a new
+ // input format causes the session to be replaced before it's used.
+ } else {
+ // The drm session isn't open yet.
+ return;
}
- // The drm session isn't open yet.
- return;
}
}
@@ -625,19 +657,26 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
eventDispatcher.inputFormatChanged(newFormat);
}
- @Override
- public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
- switch (messageType) {
- case C.MSG_SET_VOLUME:
- audioSink.setVolume((Float) message);
- break;
- case C.MSG_SET_AUDIO_ATTRIBUTES:
- AudioAttributes audioAttributes = (AudioAttributes) message;
- audioSink.setAudioAttributes(audioAttributes);
- break;
- default:
- super.handleMessage(messageType, message);
- break;
+ private void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) {
+ // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314].
+ // Allow the position to jump if the first presentable input buffer has a timestamp that
+ // differs significantly from what was expected.
+ if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) {
+ currentPositionUs = buffer.timeUs;
+ }
+ allowFirstBufferPositionDiscontinuity = false;
+ }
+ }
+
+ private void updateCurrentPosition() {
+ long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
+ if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
+ currentPositionUs =
+ allowPositionDiscontinuity
+ ? newCurrentPositionUs
+ : Math.max(currentPositionUs, newCurrentPositionUs);
+ allowPositionDiscontinuity = false;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
index 1d380ef858..68089d7b41 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java
@@ -219,7 +219,18 @@ public abstract class SimpleDecoder implements DrmSe
}
+ /**
+ * Signals that the {@link DrmInitData} passed to {@link #acquireSession} does not contain does
+ * not contain scheme data for the required UUID.
+ */
+ public static final class MissingSchemeDataException extends Exception {
+
+ private MissingSchemeDataException(UUID uuid) {
+ super("Media does not support uuid: " + uuid);
+ }
+ }
+
/**
* The key to use when passing CustomData to a PlayReady instance in an optional parameter map.
*/
public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
- private static final String CENC_SCHEME_MIME_TYPE = "cenc";
/** Determines the action to be done after a session acquired. */
@Retention(RetentionPolicy.SOURCE)
@@ -109,6 +120,9 @@ public class DefaultDrmSessionManager implements DrmSe
/** Number of times to retry for initial provisioning and key request for reporting error. */
public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3;
+ private static final String TAG = "DefaultDrmSessionMgr";
+ private static final String CENC_SCHEME_MIME_TYPE = "cenc";
+
private final UUID uuid;
private final ExoMediaDrm mediaDrm;
private final MediaDrmCallback callback;
@@ -348,10 +362,20 @@ public class DefaultDrmSessionManager implements DrmSe
@Override
public boolean canAcquireSession(@NonNull DrmInitData drmInitData) {
+ if (offlineLicenseKeySetId != null) {
+ // An offline license can be restored so a session can always be acquired.
+ return true;
+ }
SchemeData schemeData = getSchemeData(drmInitData, uuid, true);
if (schemeData == null) {
- // No data for this manager's scheme.
- return false;
+ if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) {
+ // Assume scheme specific data will be added before the session is opened.
+ Log.w(
+ TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid);
+ } else {
+ // No data for this manager's scheme.
+ return false;
+ }
}
String schemeType = drmInitData.schemeType;
if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) {
@@ -381,15 +405,15 @@ public class DefaultDrmSessionManager implements DrmSe
if (offlineLicenseKeySetId == null) {
SchemeData data = getSchemeData(drmInitData, uuid, false);
if (data == null) {
- final IllegalStateException error = new IllegalStateException(
- "Media does not support uuid: " + uuid);
+ final MissingSchemeDataException error = new MissingSchemeDataException(uuid);
if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- eventListener.onDrmSessionManagerError(error);
- }
- });
+ eventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ eventListener.onDrmSessionManagerError(error);
+ }
+ });
}
return new ErrorStateDrmSession<>(new DrmSessionException(error));
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
index 73b443dcec..0c7cb0ef01 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java
@@ -22,6 +22,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
@@ -32,6 +33,58 @@ import java.util.UUID;
*/
public final class DrmInitData implements Comparator, Parcelable {
+ /**
+ * Merges {@link DrmInitData} obtained from a media manifest and a media stream.
+ *
+ * The result is generated as follows.
+ *
+ *
+ * -
+ * Include all {@link SchemeData}s from {@code manifestData} where {@link
+ * SchemeData#hasData()} is true.
+ *
+ * -
+ * Include all {@link SchemeData}s in {@code mediaData} where {@link SchemeData#hasData()} is
+ * true and for which we did not include an entry from the manifest targeting the same UUID.
+ *
+ * -
+ * If available, the scheme type from the manifest is used. If not, the scheme type from the
+ * media is used.
+ *
+ *
+ *
+ * @param manifestData DRM session acquisition data obtained from the manifest.
+ * @param mediaData DRM session acquisition data obtained from the media.
+ * @return A {@link DrmInitData} obtained from merging a media manifest and a media stream.
+ */
+ public static @Nullable DrmInitData createSessionCreationData(
+ @Nullable DrmInitData manifestData, @Nullable DrmInitData mediaData) {
+ ArrayList result = new ArrayList<>();
+ String schemeType = null;
+ if (manifestData != null) {
+ schemeType = manifestData.schemeType;
+ for (SchemeData data : manifestData.schemeDatas) {
+ if (data.hasData()) {
+ result.add(data);
+ }
+ }
+ }
+
+ if (mediaData != null) {
+ if (schemeType == null) {
+ schemeType = mediaData.schemeType;
+ }
+ int manifestDatasCount = result.size();
+ for (SchemeData data : mediaData.schemeDatas) {
+ if (data.hasData() && !containsSchemeDataWithUuid(result, manifestDatasCount, data.uuid)) {
+ result.add(data);
+ }
+ }
+ }
+
+ return result.isEmpty() ? null : new DrmInitData(schemeType, result);
+ }
+
private final SchemeData[] schemeDatas;
// Lazily initialized hashcode.
@@ -193,6 +246,18 @@ public final class DrmInitData implements Comparator, Parcelable {
};
+ // Internal methods.
+
+ private static boolean containsSchemeDataWithUuid(
+ ArrayList datas, int limit, UUID uuid) {
+ for (int i = 0; i < limit; i++) {
+ if (datas.get(i).uuid.equals(uuid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Scheme initialization data.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
index baa5589f4b..d0c66f930a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java
@@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap {
}
@Override
- public long getPosition(long timeUs) {
- return offsets[getChunkIndex(timeUs)];
+ public SeekPoints getSeekPoints(long timeUs) {
+ int chunkIndex = getChunkIndex(timeUs);
+ SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]);
+ if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]);
+ return new SeekPoints(seekPoint, nextSeekPoint);
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
index 87355a6c78..c3f6304091 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorInput.java
@@ -30,8 +30,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
- private static final byte[] SCRATCH_SPACE = new byte[4096];
+ private static final int SCRATCH_SPACE_SIZE = 4096;
+ private final byte[] scratchSpace;
private final DataSource dataSource;
private final long streamLength;
@@ -50,6 +51,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
this.position = position;
this.streamLength = length;
peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
+ scratchSpace = new byte[SCRATCH_SPACE_SIZE];
}
@Override
@@ -84,7 +86,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
int bytesSkipped = skipFromPeekBuffer(length);
if (bytesSkipped == 0) {
bytesSkipped =
- readFromDataSource(SCRATCH_SPACE, 0, Math.min(length, SCRATCH_SPACE.length), 0, true);
+ readFromDataSource(scratchSpace, 0, Math.min(length, scratchSpace.length), 0, true);
}
commitBytesRead(bytesSkipped);
return bytesSkipped;
@@ -95,8 +97,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
throws IOException, InterruptedException {
int bytesSkipped = skipFromPeekBuffer(length);
while (bytesSkipped < length && bytesSkipped != C.RESULT_END_OF_INPUT) {
- bytesSkipped = readFromDataSource(SCRATCH_SPACE, -bytesSkipped,
- Math.min(length, bytesSkipped + SCRATCH_SPACE.length), bytesSkipped, allowEndOfInput);
+ int minLength = Math.min(length, bytesSkipped + scratchSpace.length);
+ bytesSkipped =
+ readFromDataSource(scratchSpace, -bytesSkipped, minLength, bytesSkipped, allowEndOfInput);
}
commitBytesRead(bytesSkipped);
return bytesSkipped != C.RESULT_END_OF_INPUT;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
index 87165e7a9b..b85ecba3a4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java
@@ -55,13 +55,17 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory {
static {
Constructor extends Extractor> flacExtractorConstructor = null;
try {
+ // LINT.IfChange
flacExtractorConstructor =
Class.forName("com.google.android.exoplayer2.ext.flac.FlacExtractor")
- .asSubclass(Extractor.class).getConstructor();
+ .asSubclass(Extractor.class)
+ .getConstructor();
+ // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (ClassNotFoundException e) {
- // Extractor not found.
- } catch (NoSuchMethodException e) {
- // Constructor not found.
+ // Expected if the app was built without the FLAC extension.
+ } catch (Exception e) {
+ // The FLAC extension is present, but instantiation failed.
+ throw new RuntimeException("Error instantiating FLAC extension", e);
}
FLAC_EXTRACTOR_CONSTRUCTOR = flacExtractorConstructor;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
index 964c43a45a..aa718c23e5 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java
@@ -16,36 +16,36 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
/**
* Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream.
*/
public interface SeekMap {
- /**
- * A {@link SeekMap} that does not support seeking.
- */
+ /** A {@link SeekMap} that does not support seeking. */
final class Unseekable implements SeekMap {
private final long durationUs;
- private final long startPosition;
+ private final SeekPoints startSeekPoints;
/**
- * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
- * the duration is unknown.
+ * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
+ * duration is unknown.
*/
public Unseekable(long durationUs) {
this(durationUs, 0);
}
/**
- * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if
- * the duration is unknown.
+ * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
+ * duration is unknown.
* @param startPosition The position (byte offset) of the start of the media.
*/
public Unseekable(long durationUs, long startPosition) {
this.durationUs = durationUs;
- this.startPosition = startPosition;
+ startSeekPoints =
+ new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition));
}
@Override
@@ -59,17 +59,58 @@ public interface SeekMap {
}
@Override
- public long getPosition(long timeUs) {
- return startPosition;
+ public SeekPoints getSeekPoints(long timeUs) {
+ return startSeekPoints;
+ }
+ }
+
+ /** Contains one or two {@link SeekPoint}s. */
+ final class SeekPoints {
+
+ /** The first seek point. */
+ public final SeekPoint first;
+ /** The second seek point, or {@link #first} if there's only one seek point. */
+ public final SeekPoint second;
+
+ /** @param point The single seek point. */
+ public SeekPoints(SeekPoint point) {
+ this(point, point);
}
+ /**
+ * @param first The first seek point.
+ * @param second The second seek point.
+ */
+ public SeekPoints(SeekPoint first, SeekPoint second) {
+ this.first = Assertions.checkNotNull(first);
+ this.second = Assertions.checkNotNull(second);
+ }
+
+ @Override
+ public String toString() {
+ return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekPoints other = (SeekPoints) obj;
+ return first.equals(other.first) && second.equals(other.second);
+ }
+
+ @Override
+ public int hashCode() {
+ return (31 * first.hashCode()) + second.hashCode();
+ }
}
/**
* Returns whether seeking is supported.
- *
- * If seeking is not supported then the only valid seek position is the start of the file, and so
- * {@link #getPosition(long)} will return 0 for all input values.
*
* @return Whether seeking is supported.
*/
@@ -78,20 +119,22 @@ public interface SeekMap {
/**
* Returns the duration of the stream in microseconds.
*
- * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the
- * duration is unknown.
+ * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is
+ * unknown.
*/
long getDurationUs();
/**
- * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream
- * from which data can be provided to the extractor.
+ * Obtains seek points for the specified seek time in microseconds. The returned {@link
+ * SeekPoints} will contain one or two distinct seek points.
*
- * @param timeUs A seek position in microseconds.
- * @return The corresponding position (byte offset) in the stream from which data can be provided
- * to the extractor. If {@link #isSeekable()} returns false then the returned value will be
- * independent of {@code timeUs}, and will indicate the start of the media in the stream.
+ *
Two seek points [A, B] are returned in the case that seeking can only be performed to
+ * discrete points in time, there does not exist a seek point at exactly the requested time, and
+ * there exist seek points on both sides of it. In this case A and B are the closest seek points
+ * before and after the requested time. A single seek point is returned in all other cases.
+ *
+ * @param timeUs A seek time in microseconds.
+ * @return The corresponding seek points.
*/
- long getPosition(long timeUs);
-
+ SeekPoints getSeekPoints(long timeUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java
new file mode 100644
index 0000000000..93cfbd9200
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+/** Defines a seek point in a media stream. */
+public final class SeekPoint {
+
+ /** A {@link SeekPoint} whose time and byte offset are both set to 0. */
+ public static final SeekPoint START = new SeekPoint(0, 0);
+
+ /** The time of the seek point, in microseconds. */
+ public final long timeUs;
+
+ /** The byte offset of the seek point. */
+ public final long position;
+
+ /**
+ * @param timeUs The time of the seek point, in microseconds.
+ * @param position The byte offset of the seek point.
+ */
+ public SeekPoint(long timeUs, long position) {
+ this.timeUs = timeUs;
+ this.position = position;
+ }
+
+ @Override
+ public String toString() {
+ return "[timeUs=" + timeUs + ", position=" + position + "]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ SeekPoint other = (SeekPoint) obj;
+ return timeUs == other.timeUs && position == other.position;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (int) timeUs;
+ result = 31 * result + (int) position;
+ return result;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
index 4b0bbda275..57128f45f0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.java
@@ -16,11 +16,13 @@
package com.google.android.exoplayer2.extractor.mkv;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
+import com.google.android.exoplayer2.audio.Ac3Util;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.extractor.ChunkIndex;
@@ -32,6 +34,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput;
+import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.LongArray;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil;
@@ -413,6 +416,9 @@ public final class MatroskaExtractor implements Extractor {
reader.reset();
varintReader.reset();
resetSample();
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).reset();
+ }
}
@Override
@@ -431,7 +437,13 @@ public final class MatroskaExtractor implements Extractor {
return Extractor.RESULT_SEEK;
}
}
- return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT;
+ if (!continueReading) {
+ for (int i = 0; i < tracks.size(); i++) {
+ tracks.valueAt(i).outputPendingSampleMetadata();
+ }
+ return Extractor.RESULT_END_OF_INPUT;
+ }
+ return Extractor.RESULT_CONTINUE;
}
/* package */ int getElementType(int id) {
@@ -1077,14 +1089,26 @@ public final class MatroskaExtractor implements Extractor {
}
private void commitSampleToOutput(Track track, long timeUs) {
- if (CODEC_ID_SUBRIP.equals(track.codecId)) {
- commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET,
- SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY);
- } else if (CODEC_ID_ASS.equals(track.codecId)) {
- commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET,
- SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY);
+ if (track.trueHdSampleRechunker != null) {
+ track.trueHdSampleRechunker.sampleMetadata(track, timeUs);
+ } else {
+ if (CODEC_ID_SUBRIP.equals(track.codecId)) {
+ commitSubtitleSample(
+ track,
+ SUBRIP_TIMECODE_FORMAT,
+ SUBRIP_PREFIX_END_TIMECODE_OFFSET,
+ SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR,
+ SUBRIP_TIMECODE_EMPTY);
+ } else if (CODEC_ID_ASS.equals(track.codecId)) {
+ commitSubtitleSample(
+ track,
+ SSA_TIMECODE_FORMAT,
+ SSA_PREFIX_END_TIMECODE_OFFSET,
+ SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR,
+ SSA_TIMECODE_EMPTY);
+ }
+ track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);
}
- track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData);
sampleRead = true;
resetSample();
}
@@ -1251,6 +1275,10 @@ public final class MatroskaExtractor implements Extractor {
}
}
} else {
+ if (track.trueHdSampleRechunker != null) {
+ Assertions.checkState(sampleStrippedBytes.limit() == 0);
+ track.trueHdSampleRechunker.startSample(input, blockFlags, size);
+ }
while (sampleBytesRead < size) {
readToOutput(input, output, size - sampleBytesRead);
}
@@ -1510,7 +1538,70 @@ public final class MatroskaExtractor implements Extractor {
throws IOException, InterruptedException {
MatroskaExtractor.this.binaryElement(id, contentsSize, input);
}
+ }
+ /**
+ * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples.
+ */
+ private static final class TrueHdSampleRechunker {
+
+ private final byte[] syncframePrefix;
+
+ private boolean foundSyncframe;
+ private int sampleCount;
+ private int chunkSize;
+ private long timeUs;
+ private @C.BufferFlags int blockFlags;
+
+ public TrueHdSampleRechunker() {
+ syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH];
+ }
+
+ public void reset() {
+ foundSyncframe = false;
+ }
+
+ public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size)
+ throws IOException, InterruptedException {
+ if (!foundSyncframe) {
+ input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH);
+ input.resetPeekPosition();
+ if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) {
+ return;
+ }
+ foundSyncframe = true;
+ sampleCount = 0;
+ }
+ if (sampleCount == 0) {
+ // This is the first sample in the chunk, so reset the block flags and chunk size.
+ this.blockFlags = blockFlags;
+ chunkSize = 0;
+ }
+ chunkSize += size;
+ }
+
+ public void sampleMetadata(Track track, long timeUs) {
+ if (!foundSyncframe) {
+ return;
+ }
+ if (sampleCount++ == 0) {
+ // This is the first sample in the chunk, so update the timestamp.
+ this.timeUs = timeUs;
+ }
+ if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) {
+ // We haven't read enough samples to output a chunk.
+ return;
+ }
+ track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData);
+ sampleCount = 0;
+ }
+
+ public void outputPendingSampleMetadata(Track track) {
+ if (foundSyncframe && sampleCount > 0) {
+ track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData);
+ sampleCount = 0;
+ }
+ }
}
private static final class Track {
@@ -1573,6 +1664,7 @@ public final class MatroskaExtractor implements Extractor {
public int sampleRate = 8000;
public long codecDelayNs = 0;
public long seekPreRollNs = 0;
+ @Nullable public TrueHdSampleRechunker trueHdSampleRechunker;
// Text elements.
public boolean flagForced;
@@ -1583,9 +1675,7 @@ public final class MatroskaExtractor implements Extractor {
public TrackOutput output;
public int nalUnitLengthFieldLength;
- /**
- * Initializes the track with an output.
- */
+ /** Initializes the track with an output. */
public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException {
String mimeType;
int maxInputSize = Format.NO_VALUE;
@@ -1669,6 +1759,7 @@ public final class MatroskaExtractor implements Extractor {
break;
case CODEC_ID_TRUEHD:
mimeType = MimeTypes.AUDIO_TRUEHD;
+ trueHdSampleRechunker = new TrueHdSampleRechunker();
break;
case CODEC_ID_DTS:
case CODEC_ID_DTS_EXPRESS:
@@ -1776,8 +1867,16 @@ public final class MatroskaExtractor implements Extractor {
|| MimeTypes.APPLICATION_PGS.equals(mimeType)
|| MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
- format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
- Format.NO_VALUE, initializationData, language, drmInitData);
+ format =
+ Format.createImageSampleFormat(
+ Integer.toString(trackId),
+ mimeType,
+ null,
+ Format.NO_VALUE,
+ selectionFlags,
+ initializationData,
+ language,
+ drmInitData);
} else {
throw new ParserException("Unexpected MIME type.");
}
@@ -1786,9 +1885,21 @@ public final class MatroskaExtractor implements Extractor {
this.output.format(format);
}
- /**
- * Returns the HDR Static Info as defined in CTA-861.3.
- */
+ /** Forces any pending sample metadata to be flushed to the output. */
+ public void outputPendingSampleMetadata() {
+ if (trueHdSampleRechunker != null) {
+ trueHdSampleRechunker.outputPendingSampleMetadata(this);
+ }
+ }
+
+ /** Resets any state stored in the track in response to a seek. */
+ public void reset() {
+ if (trueHdSampleRechunker != null) {
+ trueHdSampleRechunker.reset();
+ }
+ }
+
+ /** Returns the HDR Static Info as defined in CTA-861.3. */
private byte[] getHdrStaticInfo() {
// Are all fields present.
if (primaryRChromaticityX == Format.NO_VALUE || primaryRChromaticityY == Format.NO_VALUE
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
index 442e62deca..d358c0cae1 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeeker.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util;
/**
@@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
- public long getPosition(long timeUs) {
+ public SeekPoints getSeekPoints(long timeUs) {
if (dataSize == C.LENGTH_UNSET) {
- return firstFramePosition;
+ return new SeekPoints(new SeekPoint(0, firstFramePosition));
}
long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE);
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / frameSize) * frameSize;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize);
- // Add data start position.
- return firstFramePosition + positionOffset;
+ long seekPosition = firstFramePosition + positionOffset;
+ long seekTimeUs = getTimeUs(seekPosition);
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
+ if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondSeekPosition = seekPosition + frameSize;
+ long secondSeekTimeUs = getTimeUs(secondSeekPosition);
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
index cc631d9f7e..f918b5c43d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/VbriSeeker.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
- public long getPosition(long timeUs) {
- return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)];
+ public SeekPoints getSeekPoints(long timeUs) {
+ int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true);
+ SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]);
+ if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]);
+ return new SeekPoints(seekPoint, nextSeekPoint);
+ }
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
index e532249a64..a3bd5a2da2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp3/XingSeeker.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.MpegAudioHeader;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
@@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
- public long getPosition(long timeUs) {
+ public SeekPoints getSeekPoints(long timeUs) {
if (!isSeekable()) {
- return dataStartPosition + xingFrameSize;
+ return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize));
}
+ timeUs = Util.constrainValue(timeUs, 0, durationUs);
double percent = (timeUs * 100d) / durationUs;
double scaledPosition;
if (percent <= 0) {
@@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util;
long positionOffset = Math.round((scaledPosition / 256) * dataSize);
// Ensure returned positions skip the frame containing the XING header.
positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1);
- return dataStartPosition + positionOffset;
+ return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset));
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
index 28a1ffaa7b..7e40f6d2ee 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/FragmentedMp4Extractor.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -44,10 +45,10 @@ import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.UUID;
@@ -108,6 +109,8 @@ public final class FragmentedMp4Extractor implements Extractor {
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
+ private static final Format EMSG_FORMAT =
+ Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE);
// Parser states.
private static final int STATE_READING_ATOM_HEADER = 0;
@@ -141,7 +144,8 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch;
private final Stack containerAtoms;
- private final LinkedList pendingMetadataSampleInfos;
+ private final ArrayDeque pendingMetadataSampleInfos;
+ private final @Nullable TrackOutput additionalEmsgTrackOutput;
private int parserState;
private int atomType;
@@ -161,7 +165,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Extractor output.
private ExtractorOutput extractorOutput;
- private TrackOutput eventMessageTrackOutput;
+ private TrackOutput[] emsgTrackOutputs;
private TrackOutput[] cea608TrackOutputs;
// Whether extractorOutput.seekMap has been called.
@@ -212,11 +216,32 @@ public final class FragmentedMp4Extractor implements Extractor {
*/
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) {
+ this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData,
+ closedCaptionFormats, null);
+ }
+
+ /**
+ * @param flags Flags that control the extractor's behavior.
+ * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
+ * @param sideloadedTrack Sideloaded track information, in the case that the extractor
+ * will not receive a moov box in the input data. Null if a moov box is expected.
+ * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the
+ * pssh boxes (if present) will be used.
+ * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed
+ * caption channels to expose.
+ * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages
+ * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special
+ * handling of emsg messages for players is not required.
+ */
+ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
+ Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats,
+ @Nullable TrackOutput additionalEmsgTrackOutput) {
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
this.timestampAdjuster = timestampAdjuster;
this.sideloadedTrack = sideloadedTrack;
this.sideloadedDrmInitData = sideloadedDrmInitData;
this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats);
+ this.additionalEmsgTrackOutput = additionalEmsgTrackOutput;
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalPrefix = new ParsableByteArray(5);
@@ -225,7 +250,7 @@ public final class FragmentedMp4Extractor implements Extractor {
defaultInitializationVector = new ParsableByteArray();
extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>();
- pendingMetadataSampleInfos = new LinkedList<>();
+ pendingMetadataSampleInfos = new ArrayDeque<>();
trackBundles = new SparseArray<>();
durationUs = C.TIME_UNSET;
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
@@ -494,10 +519,21 @@ public final class FragmentedMp4Extractor implements Extractor {
}
private void maybeInitExtraTracks() {
- if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
- eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);
- eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
- Format.OFFSET_SAMPLE_RELATIVE));
+ if (emsgTrackOutputs == null) {
+ emsgTrackOutputs = new TrackOutput[2];
+ int emsgTrackOutputCount = 0;
+ if (additionalEmsgTrackOutput != null) {
+ emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput;
+ }
+ if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) {
+ emsgTrackOutputs[emsgTrackOutputCount++] =
+ extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);
+ }
+ emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount);
+
+ for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) {
+ eventMessageTrackOutput.format(EMSG_FORMAT);
+ }
}
if (cea608TrackOutputs == null) {
cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()];
@@ -510,29 +546,34 @@ public final class FragmentedMp4Extractor implements Extractor {
}
/**
- * Handles an emsg atom (defined in 23009-1).
+ * Parses an emsg atom (defined in 23009-1).
*/
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
- if (eventMessageTrackOutput == null) {
+ if (emsgTrackOutputs == null || emsgTrackOutputs.length == 0) {
return;
}
- // Parse the event's presentation time delta.
+
atom.setPosition(Atom.FULL_HEADER_SIZE);
+ int sampleSize = atom.bytesLeft();
atom.readNullTerminatedString(); // schemeIdUri
atom.readNullTerminatedString(); // value
long timescale = atom.readUnsignedInt();
long presentationTimeDeltaUs =
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
+
// Output the sample data.
- atom.setPosition(Atom.FULL_HEADER_SIZE);
- int sampleSize = atom.bytesLeft();
- eventMessageTrackOutput.sampleData(atom, sampleSize);
+ for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
+ atom.setPosition(Atom.FULL_HEADER_SIZE);
+ emsgTrackOutput.sampleData(atom, sampleSize);
+ }
+
// Output the sample metadata.
if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
- // We can output the sample metadata immediately.
- eventMessageTrackOutput.sampleMetadata(
- segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
- C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
+ for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
+ emsgTrackOutput.sampleMetadata(
+ segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
+ C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
+ }
} else {
// We need the first sample timestamp in the segment before we can output the metadata.
pendingMetadataSampleInfos.addLast(
@@ -1194,13 +1235,8 @@ public final class FragmentedMp4Extractor implements Extractor {
output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData);
- while (!pendingMetadataSampleInfos.isEmpty()) {
- MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
- pendingMetadataSampleBytes -= sampleInfo.size;
- eventMessageTrackOutput.sampleMetadata(
- sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
- C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
- }
+ // After we have the sampleTimeUs, we can commit all the pending metadata samples
+ outputPendingMetadataSamples(sampleTimeUs);
currentTrackBundle.currentSampleIndex++;
currentTrackBundle.currentSampleInTrackRun++;
@@ -1214,6 +1250,18 @@ public final class FragmentedMp4Extractor implements Extractor {
return true;
}
+ private void outputPendingMetadataSamples(long sampleTimeUs) {
+ while (!pendingMetadataSampleInfos.isEmpty()) {
+ MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
+ pendingMetadataSampleBytes -= sampleInfo.size;
+ for (TrackOutput emsgTrackOutput : emsgTrackOutputs) {
+ emsgTrackOutput.sampleMetadata(
+ sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
+ C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
+ }
+ }
+ }
+
/**
* Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those
* yet to be consumed, or null if all have been consumed.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
index f2412bf4ba..112c2d1ba0 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/Mp4Extractor.java
@@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.GaplessInfoHolder;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.metadata.Metadata;
@@ -87,6 +88,12 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/
private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024;
+ /**
+ * For poorly interleaved streams, the maximum byte difference one track is allowed to be read
+ * ahead before the source will be reloaded at a new position to read another track.
+ */
+ private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024;
+
private final @Flags int flags;
// Temporary arrays.
@@ -102,12 +109,15 @@ public final class Mp4Extractor implements Extractor, SeekMap {
private int atomHeaderBytesRead;
private ParsableByteArray atomData;
+ private int sampleTrackIndex;
private int sampleBytesWritten;
private int sampleCurrentNalBytesRemaining;
// Extractor outputs.
private ExtractorOutput extractorOutput;
private Mp4Track[] tracks;
+ private long[][] accumulatedSampleSizes;
+ private int firstVideoTrackIndex;
private long durationUs;
private boolean isQuickTime;
@@ -130,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
containerAtoms = new Stack<>();
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4);
+ sampleTrackIndex = C.INDEX_UNSET;
}
@Override
@@ -146,6 +157,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
public void seek(long position, long timeUs) {
containerAtoms.clear();
atomHeaderBytesRead = 0;
+ sampleTrackIndex = C.INDEX_UNSET;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
if (position == 0) {
@@ -196,21 +208,56 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
@Override
- public long getPosition(long timeUs) {
- long earliestSamplePosition = Long.MAX_VALUE;
- for (Mp4Track track : tracks) {
- TrackSampleTable sampleTable = track.sampleTable;
- int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ public SeekPoints getSeekPoints(long timeUs) {
+ if (tracks.length == 0) {
+ return new SeekPoints(SeekPoint.START);
+ }
+
+ long firstTimeUs;
+ long firstOffset;
+ long secondTimeUs = C.TIME_UNSET;
+ long secondOffset = C.POSITION_UNSET;
+
+ // If we have a video track, use it to establish one or two seek points.
+ if (firstVideoTrackIndex != C.INDEX_UNSET) {
+ TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable;
+ int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs);
if (sampleIndex == C.INDEX_UNSET) {
- // Handle the case where the requested time is before the first synchronization sample.
- sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ return new SeekPoints(SeekPoint.START);
}
- long offset = sampleTable.offsets[sampleIndex];
- if (offset < earliestSamplePosition) {
- earliestSamplePosition = offset;
+ long sampleTimeUs = sampleTable.timestampsUs[sampleIndex];
+ firstTimeUs = sampleTimeUs;
+ firstOffset = sampleTable.offsets[sampleIndex];
+ if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) {
+ int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) {
+ secondTimeUs = sampleTable.timestampsUs[secondSampleIndex];
+ secondOffset = sampleTable.offsets[secondSampleIndex];
+ }
+ }
+ } else {
+ firstTimeUs = timeUs;
+ firstOffset = Long.MAX_VALUE;
+ }
+
+ // Take into account other tracks.
+ for (int i = 0; i < tracks.length; i++) {
+ if (i != firstVideoTrackIndex) {
+ TrackSampleTable sampleTable = tracks[i].sampleTable;
+ firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset);
+ if (secondTimeUs != C.TIME_UNSET) {
+ secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset);
+ }
}
}
- return earliestSamplePosition;
+
+ SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset);
+ if (secondTimeUs == C.TIME_UNSET) {
+ return new SeekPoints(firstSeekPoint);
+ } else {
+ SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset);
+ return new SeekPoints(firstSeekPoint, secondSeekPoint);
+ }
}
// Private methods.
@@ -326,34 +373,13 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
- /**
- * Process an ftyp atom to determine whether the media is QuickTime.
- *
- * @param atomData The ftyp atom data.
- * @return Whether the media is QuickTime.
- */
- private static boolean processFtypAtom(ParsableByteArray atomData) {
- atomData.setPosition(Atom.HEADER_SIZE);
- int majorBrand = atomData.readInt();
- if (majorBrand == BRAND_QUICKTIME) {
- return true;
- }
- atomData.skipBytes(4); // minor_version
- while (atomData.bytesLeft() > 0) {
- if (atomData.readInt() == BRAND_QUICKTIME) {
- return true;
- }
- }
- return false;
- }
-
/**
* Updates the stored track metadata to reflect the contents of the specified moov atom.
*/
private void processMoovAtom(ContainerAtom moov) throws ParserException {
+ int firstVideoTrackIndex = C.INDEX_UNSET;
long durationUs = C.TIME_UNSET;
List tracks = new ArrayList<>();
- long earliestSampleOffset = Long.MAX_VALUE;
Metadata metadata = null;
GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder();
@@ -402,15 +428,16 @@ public final class Mp4Extractor implements Extractor, SeekMap {
mp4Track.trackOutput.format(format);
durationUs = Math.max(durationUs, track.durationUs);
- tracks.add(mp4Track);
-
- long firstSampleOffset = trackSampleTable.offsets[0];
- if (firstSampleOffset < earliestSampleOffset) {
- earliestSampleOffset = firstSampleOffset;
+ if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) {
+ firstVideoTrackIndex = tracks.size();
}
+ tracks.add(mp4Track);
}
+ this.firstVideoTrackIndex = firstVideoTrackIndex;
this.durationUs = durationUs;
this.tracks = tracks.toArray(new Mp4Track[tracks.size()]);
+ accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks);
+
extractorOutput.endTracks();
extractorOutput.seekMap(this);
}
@@ -433,26 +460,29 @@ public final class Mp4Extractor implements Extractor, SeekMap {
*/
private int readSample(ExtractorInput input, PositionHolder positionHolder)
throws IOException, InterruptedException {
- int trackIndex = getTrackIndexOfEarliestCurrentSample();
- if (trackIndex == C.INDEX_UNSET) {
- return RESULT_END_OF_INPUT;
+ long inputPosition = input.getPosition();
+ if (sampleTrackIndex == C.INDEX_UNSET) {
+ sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition);
+ if (sampleTrackIndex == C.INDEX_UNSET) {
+ return RESULT_END_OF_INPUT;
+ }
}
- Mp4Track track = tracks[trackIndex];
+ Mp4Track track = tracks[sampleTrackIndex];
TrackOutput trackOutput = track.trackOutput;
int sampleIndex = track.sampleIndex;
long position = track.sampleTable.offsets[sampleIndex];
int sampleSize = track.sampleTable.sizes[sampleIndex];
- if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
- // The sample information is contained in a cdat atom. The header must be discarded for
- // committing.
- position += Atom.HEADER_SIZE;
- sampleSize -= Atom.HEADER_SIZE;
- }
- long skipAmount = position - input.getPosition() + sampleBytesWritten;
+ long skipAmount = position - inputPosition + sampleBytesWritten;
if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) {
positionHolder.position = position;
return RESULT_SEEK;
}
+ if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) {
+ // The sample information is contained in a cdat atom. The header must be discarded for
+ // committing.
+ skipAmount += Atom.HEADER_SIZE;
+ sampleSize -= Atom.HEADER_SIZE;
+ }
input.skipFully((int) skipAmount);
if (track.track.nalUnitLengthFieldLength != 0) {
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
@@ -494,33 +524,61 @@ public final class Mp4Extractor implements Extractor, SeekMap {
trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex],
track.sampleTable.flags[sampleIndex], sampleSize, 0, null);
track.sampleIndex++;
+ sampleTrackIndex = C.INDEX_UNSET;
sampleBytesWritten = 0;
sampleCurrentNalBytesRemaining = 0;
return RESULT_CONTINUE;
}
/**
- * Returns the index of the track that contains the earliest current sample, or
- * {@link C#INDEX_UNSET} if no samples remain.
+ * Returns the index of the track that contains the next sample to be read, or {@link
+ * C#INDEX_UNSET} if no samples remain.
+ *
+ * The preferred choice is the sample with the smallest offset not requiring a source reload,
+ * or if not available the sample with the smallest overall offset to avoid subsequent source
+ * reloads.
+ *
+ *
To deal with poor sample interleaving, we also check whether the required memory to catch up
+ * with the next logical sample (based on sample time) exceeds {@link
+ * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even
+ * though it may require a source reload.
*/
- private int getTrackIndexOfEarliestCurrentSample() {
- int earliestSampleTrackIndex = C.INDEX_UNSET;
- long earliestSampleOffset = Long.MAX_VALUE;
+ private int getTrackIndexOfNextReadSample(long inputPosition) {
+ long preferredSkipAmount = Long.MAX_VALUE;
+ boolean preferredRequiresReload = true;
+ int preferredTrackIndex = C.INDEX_UNSET;
+ long preferredAccumulatedBytes = Long.MAX_VALUE;
+ long minAccumulatedBytes = Long.MAX_VALUE;
+ boolean minAccumulatedBytesRequiresReload = true;
+ int minAccumulatedBytesTrackIndex = C.INDEX_UNSET;
for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) {
Mp4Track track = tracks[trackIndex];
int sampleIndex = track.sampleIndex;
if (sampleIndex == track.sampleTable.sampleCount) {
continue;
}
-
- long trackSampleOffset = track.sampleTable.offsets[sampleIndex];
- if (trackSampleOffset < earliestSampleOffset) {
- earliestSampleOffset = trackSampleOffset;
- earliestSampleTrackIndex = trackIndex;
+ long sampleOffset = track.sampleTable.offsets[sampleIndex];
+ long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex];
+ long skipAmount = sampleOffset - inputPosition;
+ boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE;
+ if ((!requiresReload && preferredRequiresReload)
+ || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) {
+ preferredRequiresReload = requiresReload;
+ preferredSkipAmount = skipAmount;
+ preferredTrackIndex = trackIndex;
+ preferredAccumulatedBytes = sampleAccumulatedBytes;
+ }
+ if (sampleAccumulatedBytes < minAccumulatedBytes) {
+ minAccumulatedBytes = sampleAccumulatedBytes;
+ minAccumulatedBytesRequiresReload = requiresReload;
+ minAccumulatedBytesTrackIndex = trackIndex;
}
}
-
- return earliestSampleTrackIndex;
+ return minAccumulatedBytes == Long.MAX_VALUE
+ || !minAccumulatedBytesRequiresReload
+ || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM
+ ? preferredTrackIndex
+ : minAccumulatedBytesTrackIndex;
}
/**
@@ -538,6 +596,105 @@ public final class Mp4Extractor implements Extractor, SeekMap {
}
}
+ /**
+ * For each sample of each track, calculates accumulated size of all samples which need to be read
+ * before this sample can be used.
+ */
+ private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) {
+ long[][] accumulatedSampleSizes = new long[tracks.length][];
+ int[] nextSampleIndex = new int[tracks.length];
+ long[] nextSampleTimesUs = new long[tracks.length];
+ boolean[] tracksFinished = new boolean[tracks.length];
+ for (int i = 0; i < tracks.length; i++) {
+ accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount];
+ nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0];
+ }
+ long accumulatedSampleSize = 0;
+ int finishedTracks = 0;
+ while (finishedTracks < tracks.length) {
+ long minTimeUs = Long.MAX_VALUE;
+ int minTimeTrackIndex = -1;
+ for (int i = 0; i < tracks.length; i++) {
+ if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) {
+ minTimeTrackIndex = i;
+ minTimeUs = nextSampleTimesUs[i];
+ }
+ }
+ int trackSampleIndex = nextSampleIndex[minTimeTrackIndex];
+ accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize;
+ accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex];
+ nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex;
+ if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) {
+ nextSampleTimesUs[minTimeTrackIndex] =
+ tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex];
+ } else {
+ tracksFinished[minTimeTrackIndex] = true;
+ finishedTracks++;
+ }
+ }
+ return accumulatedSampleSizes;
+ }
+
+ /**
+ * Adjusts a seek point offset to take into account the track with the given {@code sampleTable},
+ * for a given {@code seekTimeUs}.
+ *
+ * @param sampleTable The sample table to use.
+ * @param seekTimeUs The seek time in microseconds.
+ * @param offset The current offset.
+ * @return The adjusted offset.
+ */
+ private static long maybeAdjustSeekOffset(
+ TrackSampleTable sampleTable, long seekTimeUs, long offset) {
+ int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ return offset;
+ }
+ long sampleOffset = sampleTable.offsets[sampleIndex];
+ return Math.min(sampleOffset, offset);
+ }
+
+ /**
+ * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of
+ * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if
+ * there are no synchronization samples in the table.
+ *
+ * @param sampleTable The sample table in which to locate a synchronization sample.
+ * @param timeUs A time in microseconds.
+ * @return The index of the synchronization sample before or at {@code timeUs}, or the index of
+ * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET}
+ * if there are no synchronization samples in the table.
+ */
+ private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) {
+ int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs);
+ if (sampleIndex == C.INDEX_UNSET) {
+ // Handle the case where the requested time is before the first synchronization sample.
+ sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs);
+ }
+ return sampleIndex;
+ }
+
+ /**
+ * Process an ftyp atom to determine whether the media is QuickTime.
+ *
+ * @param atomData The ftyp atom data.
+ * @return Whether the media is QuickTime.
+ */
+ private static boolean processFtypAtom(ParsableByteArray atomData) {
+ atomData.setPosition(Atom.HEADER_SIZE);
+ int majorBrand = atomData.readInt();
+ if (majorBrand == BRAND_QUICKTIME) {
+ return true;
+ }
+ atomData.skipBytes(4); // minor_version
+ while (atomData.bytesLeft() > 0) {
+ if (atomData.readInt() == BRAND_QUICKTIME) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns whether the extractor should decode a leaf atom with type {@code atom}.
*/
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
index 77def57275..042ab681f9 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeeker.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Assertions;
import java.io.EOFException;
import java.io.IOException;
@@ -219,12 +220,13 @@ import java.io.IOException;
}
@Override
- public long getPosition(long timeUs) {
+ public SeekPoints getSeekPoints(long timeUs) {
if (timeUs == 0) {
- return startPosition;
+ return new SeekPoints(new SeekPoint(0, startPosition));
}
long granule = streamReader.convertTimeToGranule(timeUs);
- return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
+ long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET);
+ return new SeekPoints(new SeekPoint(timeUs, estimatedPosition));
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
index 304fb3dd96..5eb0727908 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ogg/FlacReader.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.FlacStreamInfo;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
@@ -192,10 +193,20 @@ import java.util.List;
}
@Override
- public long getPosition(long timeUs) {
+ public SeekPoints getSeekPoints(long timeUs) {
long granule = convertTimeToGranule(timeUs);
int index = Util.binarySearchFloor(seekPointGranules, granule, true, true);
- return firstFrameOffset + seekPointOffsets[index];
+ long seekTimeUs = convertGranuleToTime(seekPointGranules[index]);
+ long seekPosition = firstFrameOffset + seekPointOffsets[index];
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
+ if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]);
+ long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1];
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
}
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
index df1e8816f0..0fc3383015 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DtsReader.java
@@ -32,9 +32,7 @@ public final class DtsReader implements ElementaryStreamReader {
private static final int STATE_READING_HEADER = 1;
private static final int STATE_READING_SAMPLE = 2;
- private static final int HEADER_SIZE = 15;
- private static final int SYNC_VALUE = 0x7FFE8001;
- private static final int SYNC_VALUE_SIZE = 4;
+ private static final int HEADER_SIZE = 18;
private final ParsableByteArray headerScratchBytes;
private final String language;
@@ -63,10 +61,6 @@ public final class DtsReader implements ElementaryStreamReader {
*/
public DtsReader(String language) {
headerScratchBytes = new ParsableByteArray(new byte[HEADER_SIZE]);
- headerScratchBytes.data[0] = (byte) ((SYNC_VALUE >> 24) & 0xFF);
- headerScratchBytes.data[1] = (byte) ((SYNC_VALUE >> 16) & 0xFF);
- headerScratchBytes.data[2] = (byte) ((SYNC_VALUE >> 8) & 0xFF);
- headerScratchBytes.data[3] = (byte) (SYNC_VALUE & 0xFF);
state = STATE_FINDING_SYNC;
this.language = language;
}
@@ -96,7 +90,6 @@ public final class DtsReader implements ElementaryStreamReader {
switch (state) {
case STATE_FINDING_SYNC:
if (skipToNextSync(data)) {
- bytesRead = SYNC_VALUE_SIZE;
state = STATE_READING_HEADER;
}
break;
@@ -154,7 +147,12 @@ public final class DtsReader implements ElementaryStreamReader {
while (pesBuffer.bytesLeft() > 0) {
syncBytes <<= 8;
syncBytes |= pesBuffer.readUnsignedByte();
- if (syncBytes == SYNC_VALUE) {
+ if (DtsUtil.isSyncWord(syncBytes)) {
+ headerScratchBytes.data[0] = (byte) ((syncBytes >> 24) & 0xFF);
+ headerScratchBytes.data[1] = (byte) ((syncBytes >> 16) & 0xFF);
+ headerScratchBytes.data[2] = (byte) ((syncBytes >> 8) & 0xFF);
+ headerScratchBytes.data[3] = (byte) (syncBytes & 0xFF);
+ bytesRead = 4;
syncBytes = 0;
return true;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
index e00c63a354..0944d1810e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DvbSubtitleReader.java
@@ -58,9 +58,16 @@ public final class DvbSubtitleReader implements ElementaryStreamReader {
DvbSubtitleInfo subtitleInfo = subtitleInfos.get(i);
idGenerator.generateNewId();
TrackOutput output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_TEXT);
- output.format(Format.createImageSampleFormat(idGenerator.getFormatId(),
- MimeTypes.APPLICATION_DVBSUBS, null, Format.NO_VALUE,
- Collections.singletonList(subtitleInfo.initializationData), subtitleInfo.language, null));
+ output.format(
+ Format.createImageSampleFormat(
+ idGenerator.getFormatId(),
+ MimeTypes.APPLICATION_DVBSUBS,
+ null,
+ Format.NO_VALUE,
+ 0,
+ Collections.singletonList(subtitleInfo.initializationData),
+ subtitleInfo.language,
+ null));
outputs[i] = output;
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
index d06c6f0cb4..313e556764 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java
@@ -61,7 +61,6 @@ public final class LatmReader implements ElementaryStreamReader {
// Container data.
private boolean streamMuxRead;
- private int audioMuxVersion;
private int audioMuxVersionA;
private int numSubframes;
private int frameLengthType;
@@ -176,7 +175,7 @@ public final class LatmReader implements ElementaryStreamReader {
* 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);
+ int audioMuxVersion = data.readBits(1);
audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0;
if (audioMuxVersionA == 0) {
if (audioMuxVersion == 1) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
index 13e669da23..50931e2d90 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java
@@ -20,6 +20,7 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
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;
@@ -122,6 +123,7 @@ public final class TsExtractor implements Extractor {
private int remainingPmts;
private boolean tracksEnded;
private TsPayloadReader id3Reader;
+ private int bytesSinceLastSync;
public TsExtractor() {
this(0);
@@ -163,7 +165,7 @@ public final class TsExtractor implements Extractor {
timestampAdjusters = new ArrayList<>();
timestampAdjusters.add(timestampAdjuster);
}
- tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE);
+ tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0);
trackIds = new SparseBooleanArray();
tsPayloadReaders = new SparseArray<>();
continuityCounters = new SparseIntArray();
@@ -206,6 +208,7 @@ public final class TsExtractor implements Extractor {
continuityCounters.clear();
// Elementary stream readers' state should be cleared to get consistent behaviours when seeking.
resetPayloadReaders();
+ bytesSinceLastSync = 0;
}
@Override
@@ -238,8 +241,9 @@ public final class TsExtractor implements Extractor {
}
// Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format.
- final int limit = tsPacketBuffer.limit();
+ int limit = tsPacketBuffer.limit();
int position = tsPacketBuffer.getPosition();
+ int searchStart = position;
while (position < limit && data[position] != TS_SYNC_BYTE) {
position++;
}
@@ -247,8 +251,13 @@ public final class TsExtractor implements Extractor {
int endOfPacket = position + TS_PACKET_SIZE;
if (endOfPacket > limit) {
+ bytesSinceLastSync += position - searchStart;
+ if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) {
+ throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream.");
+ }
return RESULT_CONTINUE;
}
+ bytesSinceLastSync = 0;
int tsPacketHeader = tsPacketBuffer.readInt();
if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java
index 2cdd31cb6f..33db6c1e6c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavHeader.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekPoint;
import com.google.android.exoplayer2.util.Util;
/** Header for a WAV file. */
@@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util;
}
@Override
- public long getPosition(long timeUs) {
+ public SeekPoints getSeekPoints(long timeUs) {
long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND;
// Constrain to nearest preceding frame offset.
positionOffset = (positionOffset / blockAlignment) * blockAlignment;
positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment);
- // Add data start position.
- return dataStartPosition + positionOffset;
+ long seekPosition = dataStartPosition + positionOffset;
+ long seekTimeUs = getTimeUs(seekPosition);
+ SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition);
+ if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) {
+ return new SeekPoints(seekPoint);
+ } else {
+ long secondSeekPosition = seekPosition + blockAlignment;
+ long secondSeekTimeUs = getTimeUs(secondSeekPosition);
+ SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition);
+ return new SeekPoints(seekPoint, secondSeekPoint);
+ }
}
// Misc getters.
@@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util;
* @param position The position in bytes.
*/
public long getTimeUs(long position) {
- return position * C.MICROS_PER_SECOND / averageBytesPerSecond;
+ long positionOffset = Math.max(0, position - dataStartPosition);
+ return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond;
}
/** Returns the bytes per frame of this WAV. */
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
index ef7d691c5b..2e8fc602a2 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java
@@ -224,6 +224,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private long codecHotswapDeadlineMs;
private int inputIndex;
private int outputIndex;
+ private ByteBuffer outputBuffer;
private boolean shouldSkipOutputBuffer;
private boolean codecReconfigured;
private @ReconfigurationState int codecReconfigurationState;
@@ -322,7 +323,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected abstract void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) throws DecoderQueryException;
- @SuppressWarnings("deprecation")
protected final void maybeInitCodec() throws ExoPlaybackException {
if (codec != null || format == null) {
// We have a codec already, or we don't have a format with which to instantiate one.
@@ -338,13 +338,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError();
if (drmError != null) {
- throw ExoPlaybackException.createForRenderer(drmError, getIndex());
+ // Continue for now. We may be able to avoid failure if the session recovers, or if a new
+ // input format causes the session to be replaced before it's used.
+ } else {
+ // The drm session isn't open yet.
+ return;
}
- // The drm session isn't open yet.
- return;
+ } else {
+ wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
+ drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
- wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto();
- drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
}
if (codecInfo == null) {
@@ -399,16 +402,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
onCodecInitialized(codecName, codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
- inputBuffers = codec.getInputBuffers();
- outputBuffers = codec.getOutputBuffers();
+ getCodecBuffers();
} catch (Exception e) {
throwDecoderInitError(new DecoderInitializationException(format, e,
drmSessionRequiresSecureDecoder, codecName));
}
codecHotswapDeadlineMs = getState() == STATE_STARTED
? (SystemClock.elapsedRealtime() + MAX_CODEC_HOTSWAP_TIME_MS) : C.TIME_UNSET;
- inputIndex = C.INDEX_UNSET;
- outputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
+ resetOutputBuffer();
waitingForFirstSyncFrame = true;
decoderCounters.decoderInitCount++;
}
@@ -430,6 +432,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return codecInfo;
}
+ /**
+ * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec}
+ * for decoding the given {@link Format} for playback.
+ *
+ * @param format The format of the media.
+ * @return The framework media format.
+ */
+ protected final MediaFormat getMediaFormatForPlayback(Format format) {
+ MediaFormat mediaFormat = format.getFrameworkMediaFormatV16();
+ if (Util.SDK_INT >= 23) {
+ configureMediaFormatForPlaybackV23(mediaFormat);
+ }
+ return mediaFormat;
+ }
+
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
@@ -469,13 +486,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected void releaseCodec() {
codecHotswapDeadlineMs = C.TIME_UNSET;
- inputIndex = C.INDEX_UNSET;
- outputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
+ resetOutputBuffer();
waitingForKeys = false;
shouldSkipOutputBuffer = false;
decodeOnlyPresentationTimestamps.clear();
- inputBuffers = null;
- outputBuffers = null;
+ resetCodecBuffers();
codecInfo = null;
codecReconfigured = false;
codecReceivedBuffers = false;
@@ -490,7 +506,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecReceivedEos = false;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
- buffer.data = null;
if (codec != null) {
decoderCounters.decoderReleaseCount++;
try {
@@ -573,8 +588,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
protected void flushCodec() throws ExoPlaybackException {
codecHotswapDeadlineMs = C.TIME_UNSET;
- inputIndex = C.INDEX_UNSET;
- outputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
+ resetOutputBuffer();
waitingForFirstSyncFrame = true;
waitingForKeys = false;
shouldSkipOutputBuffer = false;
@@ -617,7 +632,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (inputIndex < 0) {
return false;
}
- buffer.data = inputBuffers[inputIndex];
+ buffer.data = getInputBuffer(inputIndex);
buffer.clear();
}
@@ -629,7 +644,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} else {
codecReceivedEos = true;
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- inputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
}
codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
return false;
@@ -639,7 +654,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecNeedsAdaptationWorkaroundBuffer = false;
buffer.data.put(ADAPTATION_WORKAROUND_BUFFER);
codec.queueInputBuffer(inputIndex, 0, ADAPTATION_WORKAROUND_BUFFER.length, 0, 0);
- inputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
codecReceivedBuffers = true;
return true;
}
@@ -697,7 +712,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} else {
codecReceivedEos = true;
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- inputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
}
} catch (CryptoException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
@@ -742,7 +757,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} else {
codec.queueInputBuffer(inputIndex, 0, buffer.data.limit(), presentationTimeUs, 0);
}
- inputIndex = C.INDEX_UNSET;
+ resetInputBuffer();
codecReceivedBuffers = true;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
decoderCounters.inputBufferCount++;
@@ -752,6 +767,50 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return true;
}
+ private void getCodecBuffers() {
+ if (Util.SDK_INT < 21) {
+ inputBuffers = codec.getInputBuffers();
+ outputBuffers = codec.getOutputBuffers();
+ }
+ }
+
+ private void resetCodecBuffers() {
+ if (Util.SDK_INT < 21) {
+ inputBuffers = null;
+ outputBuffers = null;
+ }
+ }
+
+ private ByteBuffer getInputBuffer(int inputIndex) {
+ if (Util.SDK_INT >= 21) {
+ return codec.getInputBuffer(inputIndex);
+ } else {
+ return inputBuffers[inputIndex];
+ }
+ }
+
+ private ByteBuffer getOutputBuffer(int outputIndex) {
+ if (Util.SDK_INT >= 21) {
+ return codec.getOutputBuffer(outputIndex);
+ } else {
+ return outputBuffers[outputIndex];
+ }
+ }
+
+ private boolean hasOutputBuffer() {
+ return outputIndex >= 0;
+ }
+
+ private void resetInputBuffer() {
+ inputIndex = C.INDEX_UNSET;
+ buffer.data = null;
+ }
+
+ private void resetOutputBuffer() {
+ outputIndex = C.INDEX_UNSET;
+ outputBuffer = null;
+ }
+
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(DecoderInputBuffer buffer,
int adaptiveReconfigurationBytes) {
MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16();
@@ -904,9 +963,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override
public boolean isReady() {
- return format != null && !waitingForKeys && (isSourceReady() || outputIndex >= 0
- || (codecHotswapDeadlineMs != C.TIME_UNSET
- && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));
+ return format != null
+ && !waitingForKeys
+ && (isSourceReady()
+ || hasOutputBuffer()
+ || (codecHotswapDeadlineMs != C.TIME_UNSET
+ && SystemClock.elapsedRealtime() < codecHotswapDeadlineMs));
}
/**
@@ -922,14 +984,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* @return Whether it may be possible to drain more output data.
* @throws ExoPlaybackException If an error occurs draining the output buffer.
*/
- @SuppressWarnings("deprecation")
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
- if (outputIndex < 0) {
+ if (!hasOutputBuffer()) {
+ int outputIndex;
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
try {
- outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
- getDequeueOutputBufferTimeoutUs());
+ outputIndex =
+ codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
} catch (IllegalStateException e) {
processEndOfStream();
if (outputStreamEnded) {
@@ -939,26 +1001,25 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
} else {
- outputIndex = codec.dequeueOutputBuffer(outputBufferInfo,
- getDequeueOutputBufferTimeoutUs());
+ outputIndex =
+ codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
}
+
if (outputIndex >= 0) {
// We've dequeued a buffer.
if (shouldSkipAdaptationWorkaroundOutputBuffer) {
shouldSkipAdaptationWorkaroundOutputBuffer = false;
codec.releaseOutputBuffer(outputIndex, false);
- outputIndex = C.INDEX_UNSET;
return true;
- }
- if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+ } else if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// The dequeued buffer indicates the end of the stream. Process it immediately.
processEndOfStream();
- outputIndex = C.INDEX_UNSET;
return false;
} else {
- // The dequeued buffer is a media buffer. Do some initial setup. The buffer will be
- // processed by calling processOutputBuffer (possibly multiple times) below.
- ByteBuffer outputBuffer = outputBuffers[outputIndex];
+ this.outputIndex = outputIndex;
+ outputBuffer = getOutputBuffer(outputIndex);
+ // The dequeued buffer is a media buffer. Do some initial setup.
+ // It will be processed by calling processOutputBuffer (possibly multiple times).
if (outputBuffer != null) {
outputBuffer.position(outputBufferInfo.offset);
outputBuffer.limit(outputBufferInfo.offset + outputBufferInfo.size);
@@ -972,8 +1033,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
processOutputBuffersChanged();
return true;
} else /* MediaCodec.INFO_TRY_AGAIN_LATER (-1) or unknown negative return value */ {
- if (codecNeedsEosPropagationWorkaround && (inputStreamEnded
- || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) {
+ if (codecNeedsEosPropagationWorkaround
+ && (inputStreamEnded
+ || codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) {
processEndOfStream();
}
return false;
@@ -983,9 +1045,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
boolean processedOutputBuffer;
if (codecNeedsEosOutputExceptionWorkaround && codecReceivedEos) {
try {
- processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
- outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
- outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
+ processedOutputBuffer =
+ processOutputBuffer(
+ positionUs,
+ elapsedRealtimeUs,
+ codec,
+ outputBuffer,
+ outputIndex,
+ outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs,
+ shouldSkipOutputBuffer);
} catch (IllegalStateException e) {
processEndOfStream();
if (outputStreamEnded) {
@@ -995,14 +1064,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
} else {
- processedOutputBuffer = processOutputBuffer(positionUs, elapsedRealtimeUs, codec,
- outputBuffers[outputIndex], outputIndex, outputBufferInfo.flags,
- outputBufferInfo.presentationTimeUs, shouldSkipOutputBuffer);
+ processedOutputBuffer =
+ processOutputBuffer(
+ positionUs,
+ elapsedRealtimeUs,
+ codec,
+ outputBuffer,
+ outputIndex,
+ outputBufferInfo.flags,
+ outputBufferInfo.presentationTimeUs,
+ shouldSkipOutputBuffer);
}
if (processedOutputBuffer) {
onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
- outputIndex = C.INDEX_UNSET;
+ resetOutputBuffer();
return true;
}
@@ -1030,9 +1106,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
/**
* Processes a change in the output buffers.
*/
- @SuppressWarnings("deprecation")
private void processOutputBuffersChanged() {
- outputBuffers = codec.getOutputBuffers();
+ if (Util.SDK_INT < 21) {
+ outputBuffers = codec.getOutputBuffers();
+ }
}
/**
@@ -1108,6 +1185,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return false;
}
+ @TargetApi(23)
+ private static void configureMediaFormatForPlaybackV23(MediaFormat mediaFormat) {
+ mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);
+ }
+
/**
* Returns whether the decoder is known to fail when flushed.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
index 7ae8eb3cd4..b80780884c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java
@@ -158,8 +158,8 @@ public final class MediaCodecUtil {
+ ". Assuming: " + decoderInfos.get(0).name);
}
}
- if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) {
- // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D.
+ if (MimeTypes.AUDIO_E_AC3_JOC.equals(mimeType)) {
+ // E-AC3 decoders can decode JOC streams, but in 2-D rather than 3-D.
CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure);
ArrayList eac3DecoderInfos =
getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType);
@@ -382,8 +382,8 @@ public final class MediaCodecUtil {
return false;
}
- // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041].
- if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType)
+ // MTK E-AC3 decoder doesn't support decoding JOC streams in 2-D. See [Internal: b/69400041].
+ if (MimeTypes.AUDIO_E_AC3_JOC.equals(requestedMimeType)
&& "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) {
return false;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
index fbe3184c0d..57e7f0bfd6 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessage.java
@@ -41,6 +41,13 @@ public final class EventMessage implements Metadata.Entry {
*/
public final long durationMs;
+ /**
+ * The presentation time value of this event message in microseconds.
+ *
+ * Except in special cases, application code should not use this field.
+ */
+ public final long presentationTimeUs;
+
/**
* The instance identifier.
*/
@@ -55,25 +62,27 @@ public final class EventMessage implements Metadata.Entry {
private int hashCode;
/**
- *
* @param schemeIdUri The message scheme.
* @param value The value for the event.
* @param durationMs The duration of the event in milliseconds.
* @param id The instance identifier.
* @param messageData The body of the message.
+ * @param presentationTimeUs The presentation time value of this event message in microseconds.
*/
public EventMessage(String schemeIdUri, String value, long durationMs, long id,
- byte[] messageData) {
+ byte[] messageData, long presentationTimeUs) {
this.schemeIdUri = schemeIdUri;
this.value = value;
this.durationMs = durationMs;
this.id = id;
this.messageData = messageData;
+ this.presentationTimeUs = presentationTimeUs;
}
/* package */ EventMessage(Parcel in) {
schemeIdUri = in.readString();
value = in.readString();
+ presentationTimeUs = in.readLong();
durationMs = in.readLong();
id = in.readLong();
messageData = in.createByteArray();
@@ -85,6 +94,7 @@ public final class EventMessage implements Metadata.Entry {
int result = 17;
result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
+ result = 31 * result + (int) (presentationTimeUs ^ (presentationTimeUs >>> 32));
result = 31 * result + (int) (durationMs ^ (durationMs >>> 32));
result = 31 * result + (int) (id ^ (id >>> 32));
result = 31 * result + Arrays.hashCode(messageData);
@@ -102,9 +112,9 @@ public final class EventMessage implements Metadata.Entry {
return false;
}
EventMessage other = (EventMessage) obj;
- return durationMs == other.durationMs && id == other.id
- && Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value)
- && Arrays.equals(messageData, other.messageData);
+ return presentationTimeUs == other.presentationTimeUs && durationMs == other.durationMs
+ && id == other.id && Util.areEqual(schemeIdUri, other.schemeIdUri)
+ && Util.areEqual(value, other.value) && Arrays.equals(messageData, other.messageData);
}
// Parcelable implementation.
@@ -118,6 +128,7 @@ public final class EventMessage implements Metadata.Entry {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(schemeIdUri);
dest.writeString(value);
+ dest.writeLong(presentationTimeUs);
dest.writeLong(durationMs);
dest.writeLong(id);
dest.writeByteArray(messageData);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
index fd6996aa80..7e5125e71c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoder.java
@@ -15,15 +15,17 @@
*/
package com.google.android.exoplayer2.metadata.emsg;
+import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
- * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1.
+ * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3.
*
* Atom data should be provided to the decoder without the full atom header (i.e. starting from the
* first byte of the scheme_id_uri field).
@@ -39,11 +41,13 @@ public final class EventMessageDecoder implements MetadataDecoder {
String schemeIdUri = emsgData.readNullTerminatedString();
String value = emsgData.readNullTerminatedString();
long timescale = emsgData.readUnsignedInt();
- emsgData.skipBytes(4); // presentation_time_delta
- long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
+ long presentationTimeUs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(),
+ C.MICROS_PER_SECOND, timescale);
+ long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale);
long id = emsgData.readUnsignedInt();
byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
- return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
+ return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData,
+ presentationTimeUs));
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java
new file mode 100644
index 0000000000..eca498a6df
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java
@@ -0,0 +1,85 @@
+/*
+ * 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.metadata.emsg;
+
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Encodes data that can be decoded by {@link EventMessageDecoder}. This class isn't thread safe.
+ */
+public final class EventMessageEncoder {
+
+ private final ByteArrayOutputStream byteArrayOutputStream;
+ private final DataOutputStream dataOutputStream;
+
+ public EventMessageEncoder() {
+ byteArrayOutputStream = new ByteArrayOutputStream(512);
+ dataOutputStream = new DataOutputStream(byteArrayOutputStream);
+ }
+
+ /**
+ * Encodes an {@link EventMessage} to a byte array that can be decoded by
+ * {@link EventMessageDecoder}.
+ *
+ * @param eventMessage The event message to be encoded.
+ * @param timescale Timescale of the event message, in units per second.
+ * @return The serialized byte array.
+ */
+ @Nullable
+ public byte[] encode(EventMessage eventMessage, long timescale) {
+ Assertions.checkArgument(timescale >= 0);
+ byteArrayOutputStream.reset();
+ try {
+ writeNullTerminatedString(dataOutputStream, eventMessage.schemeIdUri);
+ String nonNullValue = eventMessage.value != null ? eventMessage.value : "";
+ writeNullTerminatedString(dataOutputStream, nonNullValue);
+ writeUnsignedInt(dataOutputStream, timescale);
+ long presentationTime = Util.scaleLargeTimestamp(eventMessage.presentationTimeUs,
+ timescale, C.MICROS_PER_SECOND);
+ writeUnsignedInt(dataOutputStream, presentationTime);
+ long duration = Util.scaleLargeTimestamp(eventMessage.durationMs, timescale, 1000);
+ writeUnsignedInt(dataOutputStream, duration);
+ writeUnsignedInt(dataOutputStream, eventMessage.id);
+ dataOutputStream.write(eventMessage.messageData);
+ dataOutputStream.flush();
+ return byteArrayOutputStream.toByteArray();
+ } catch (IOException e) {
+ // Should never happen.
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static void writeNullTerminatedString(DataOutputStream dataOutputStream, String value)
+ throws IOException {
+ dataOutputStream.writeBytes(value);
+ dataOutputStream.writeByte(0);
+ }
+
+ private static void writeUnsignedInt(DataOutputStream outputStream, long value)
+ throws IOException {
+ outputStream.writeByte((int) (value >>> 24) & 0xFF);
+ outputStream.writeByte((int) (value >>> 16) & 0xFF);
+ outputStream.writeByte((int) (value >>> 8) & 0xFF);
+ outputStream.writeByte((int) value & 0xFF);
+ }
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
index 6b2e5c3675..7646af718d 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/id3/Id3Decoder.java
@@ -405,14 +405,9 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
- String value;
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
- if (valueStartIndex < data.length) {
- int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
- value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
- } else {
- value = "";
- }
+ int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
+ String value = decodeStringIfValid(data, valueStartIndex, valueEndIndex, charset);
return new TextInformationFrame("TXXX", description, value);
}
@@ -452,14 +447,9 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
- String url;
int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
- if (urlStartIndex < data.length) {
- int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
- url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1");
- } else {
- url = "";
- }
+ int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
+ String url = decodeStringIfValid(data, urlStartIndex, urlEndIndex, "ISO-8859-1");
return new UrlLinkFrame("WXXX", description, url);
}
@@ -502,13 +492,12 @@ public final class Id3Decoder implements MetadataDecoder {
int filenameStartIndex = mimeTypeEndIndex + 1;
int filenameEndIndex = indexOfEos(data, filenameStartIndex, encoding);
- String filename = new String(data, filenameStartIndex, filenameEndIndex - filenameStartIndex,
- charset);
+ String filename = decodeStringIfValid(data, filenameStartIndex, filenameEndIndex, charset);
int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding);
int descriptionEndIndex = indexOfEos(data, descriptionStartIndex, encoding);
- String description = new String(data, descriptionStartIndex,
- descriptionEndIndex - descriptionStartIndex, charset);
+ String description =
+ decodeStringIfValid(data, descriptionStartIndex, descriptionEndIndex, charset);
int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding);
byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length);
@@ -573,14 +562,9 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
- String text;
int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
- if (textStartIndex < data.length) {
- int textEndIndex = indexOfEos(data, textStartIndex, encoding);
- text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
- } else {
- text = "";
- }
+ int textEndIndex = indexOfEos(data, textStartIndex, encoding);
+ String text = decodeStringIfValid(data, textStartIndex, textEndIndex, charset);
return new CommentFrame(language, description, text);
}
@@ -760,6 +744,25 @@ public final class Id3Decoder implements MetadataDecoder {
return Arrays.copyOfRange(data, from, to);
}
+ /**
+ * Returns a string obtained by decoding the specified range of {@code data} using the specified
+ * {@code charsetName}. An empty string is returned if the range is invalid.
+ *
+ * @param data The array from which to decode the string.
+ * @param from The start of the range.
+ * @param to The end of the range (exclusive).
+ * @param charsetName The name of the Charset to use.
+ * @return The decoded string, or an empty string if the range is invalid.
+ * @throws UnsupportedEncodingException If the Charset is not supported.
+ */
+ private static String decodeStringIfValid(byte[] data, int from, int to, String charsetName)
+ throws UnsupportedEncodingException {
+ if (to <= from || to > data.length) {
+ return "";
+ }
+ return new String(data, from, to - from, charsetName);
+ }
+
private static final class Id3Header {
private final int majorVersion;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
index 3cb5db30ec..6abb950254 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java
@@ -104,10 +104,17 @@ public abstract class SegmentDownloader implements Downloader {
* previous selection is cleared. If keys are null or empty, all representations are downloaded.
*/
public final void selectRepresentations(K[] keys) {
- this.keys = keys != null ? keys.clone() : null;
+ this.keys = (keys != null && keys.length > 0) ? keys.clone() : null;
resetCounters();
}
+ /**
+ * Returns keys for all representations.
+ *
+ * @see #selectRepresentations(Object[])
+ */
+ public abstract K[] getAllRepresentationKeys() throws IOException;
+
/**
* Initializes the total segments, downloaded segments and downloaded bytes counters for the
* selected representations.
@@ -221,7 +228,7 @@ public abstract class SegmentDownloader implements Downloader {
if (manifest != null) {
List segments = null;
try {
- segments = getAllSegments(offlineDataSource, manifest, true);
+ segments = getSegments(offlineDataSource, manifest, getAllRepresentationKeys(), true);
} catch (IOException e) {
// Ignore exceptions. We do our best with what's available offline.
}
@@ -262,14 +269,6 @@ public abstract class SegmentDownloader implements Downloader {
protected abstract List getSegments(DataSource dataSource, M manifest, K[] keys,
boolean allowIncompleteIndex) throws InterruptedException, IOException;
- /**
- * Returns a list of all segments.
- *
- * @see #getSegments(DataSource, M, Object[], boolean)
- */
- protected abstract List getAllSegments(DataSource dataSource, M manifest,
- boolean allowPartialIndex) throws InterruptedException, IOException;
-
private void resetCounters() {
totalSegments = C.LENGTH_UNSET;
downloadedSegments = C.LENGTH_UNSET;
@@ -295,9 +294,10 @@ public abstract class SegmentDownloader implements Downloader {
private synchronized List initStatus(boolean offline)
throws IOException, InterruptedException {
DataSource dataSource = getDataSource(offline);
- List segments = keys != null && keys.length > 0
- ? getSegments(dataSource, manifest, keys, offline)
- : getAllSegments(dataSource, manifest, offline);
+ if (keys == null) {
+ keys = getAllRepresentationKeys();
+ }
+ List segments = getSegments(dataSource, manifest, keys, offline);
CachingCounters cachingCounters = new CachingCounters();
totalSegments = segments.size();
downloadedSegments = 0;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
index 35234753b0..696a6f6fad 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java
@@ -27,14 +27,18 @@ import com.google.android.exoplayer2.Timeline;
private final int childCount;
private final ShuffleOrder shuffleOrder;
+ private final boolean isAtomic;
/**
* Sets up a concatenated timeline with a shuffle order of child timelines.
*
+ * @param isAtomic Whether the child timelines shall be treated as atomic, i.e., treated as a
+ * single item for repeating and shuffling.
* @param shuffleOrder A shuffle order of child timelines. The number of child timelines must
* match the number of elements in the shuffle order.
*/
- public AbstractConcatenatedTimeline(ShuffleOrder shuffleOrder) {
+ public AbstractConcatenatedTimeline(boolean isAtomic, ShuffleOrder shuffleOrder) {
+ this.isAtomic = isAtomic;
this.shuffleOrder = shuffleOrder;
this.childCount = shuffleOrder.getLength();
}
@@ -42,6 +46,11 @@ import com.google.android.exoplayer2.Timeline;
@Override
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
+ if (isAtomic) {
+ // Adapt repeat and shuffle mode to atomic concatenation.
+ repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
+ shuffleModeEnabled = false;
+ }
// Find next window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
@@ -71,6 +80,11 @@ import com.google.android.exoplayer2.Timeline;
@Override
public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled) {
+ if (isAtomic) {
+ // Adapt repeat and shuffle mode to atomic concatenation.
+ repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;
+ shuffleModeEnabled = false;
+ }
// Find previous window within current child.
int childIndex = getChildIndexByWindowIndex(windowIndex);
int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);
@@ -103,6 +117,9 @@ import com.google.android.exoplayer2.Timeline;
if (childCount == 0) {
return C.INDEX_UNSET;
}
+ if (isAtomic) {
+ shuffleModeEnabled = false;
+ }
// Find last non-empty child.
int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1;
while (getTimelineByChildIndex(lastChildIndex).isEmpty()) {
@@ -121,6 +138,9 @@ import com.google.android.exoplayer2.Timeline;
if (childCount == 0) {
return C.INDEX_UNSET;
}
+ if (isAtomic) {
+ shuffleModeEnabled = false;
+ }
// Find first non-empty child.
int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0;
while (getTimelineByChildIndex(firstChildIndex).isEmpty()) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
index 1114a563b6..f14c0faad4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
@@ -36,10 +37,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
public final MediaPeriod mediaPeriod;
private MediaPeriod.Callback callback;
- private long startUs;
- private long endUs;
private ClippingSampleStream[] sampleStreams;
- private boolean pendingInitialDiscontinuity;
+ private long pendingInitialDiscontinuityPositionUs;
+ /* package */ long startUs;
+ /* package */ long endUs;
/**
* Creates a new clipping media period that provides a clipped view of the specified
@@ -57,10 +58,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
*/
public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) {
this.mediaPeriod = mediaPeriod;
+ sampleStreams = new ClippingSampleStream[0];
+ pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? 0 : C.TIME_UNSET;
startUs = C.TIME_UNSET;
endUs = C.TIME_UNSET;
- sampleStreams = new ClippingSampleStream[0];
- pendingInitialDiscontinuity = enableInitialDiscontinuity;
}
/**
@@ -95,48 +96,47 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
sampleStreams = new ClippingSampleStream[streams.length];
- SampleStream[] internalStreams = new SampleStream[streams.length];
+ SampleStream[] childStreams = new SampleStream[streams.length];
for (int i = 0; i < streams.length; i++) {
sampleStreams[i] = (ClippingSampleStream) streams[i];
- internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null;
+ childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null;
}
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
- internalStreams, streamResetFlags, positionUs + startUs);
- if (pendingInitialDiscontinuity) {
- pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections);
- }
- Assertions.checkState(enablePositionUs == positionUs + startUs
- || (enablePositionUs >= startUs
- && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
+ childStreams, streamResetFlags, positionUs + startUs) - startUs;
+ pendingInitialDiscontinuityPositionUs = isPendingInitialDiscontinuity() && positionUs == 0
+ && shouldKeepInitialDiscontinuity(startUs, selections) ? enablePositionUs : C.TIME_UNSET;
+ Assertions.checkState(enablePositionUs == positionUs
+ || (enablePositionUs >= 0
+ && (endUs == C.TIME_END_OF_SOURCE || startUs + enablePositionUs <= endUs)));
for (int i = 0; i < streams.length; i++) {
- if (internalStreams[i] == null) {
+ if (childStreams[i] == null) {
sampleStreams[i] = null;
- } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) {
- sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs,
- pendingInitialDiscontinuity);
+ } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) {
+ sampleStreams[i] = new ClippingSampleStream(childStreams[i]);
}
streams[i] = sampleStreams[i];
}
- return enablePositionUs - startUs;
+ return enablePositionUs;
}
@Override
- public void discardBuffer(long positionUs) {
- mediaPeriod.discardBuffer(positionUs + startUs);
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe);
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ mediaPeriod.reevaluateBuffer(positionUs + startUs);
}
@Override
public long readDiscontinuity() {
- if (pendingInitialDiscontinuity) {
- for (ClippingSampleStream sampleStream : sampleStreams) {
- if (sampleStream != null) {
- sampleStream.clearPendingDiscontinuity();
- }
- }
- pendingInitialDiscontinuity = false;
- // Always read an initial discontinuity, using mediaPeriod's discontinuity if set.
- long discontinuityUs = readDiscontinuity();
- return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0;
+ if (isPendingInitialDiscontinuity()) {
+ long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs;
+ pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;
+ // Always read an initial discontinuity from the child, and use it if set.
+ long childDiscontinuityUs = readDiscontinuity();
+ return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs;
}
long discontinuityUs = mediaPeriod.readDiscontinuity();
if (discontinuityUs == C.TIME_UNSET) {
@@ -159,17 +159,31 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public long seekToUs(long positionUs) {
+ pendingInitialDiscontinuityPositionUs = C.TIME_UNSET;
for (ClippingSampleStream sampleStream : sampleStreams) {
if (sampleStream != null) {
sampleStream.clearSentEos();
}
}
- long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
- Assertions.checkState(seekUs == positionUs + startUs
- || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
+ long offsetPositionUs = positionUs + startUs;
+ long seekUs = mediaPeriod.seekToUs(offsetPositionUs);
+ Assertions.checkState(
+ seekUs == offsetPositionUs
+ || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
return seekUs - startUs;
}
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ if (positionUs == startUs) {
+ // Never adjust seeks to the start of the clipped view.
+ return 0;
+ }
+ long offsetPositionUs = positionUs + startUs;
+ SeekParameters clippedSeekParameters = clipSeekParameters(offsetPositionUs, seekParameters);
+ return mediaPeriod.getAdjustedSeekPositionUs(offsetPositionUs, clippedSeekParameters) - startUs;
+ }
+
@Override
public long getNextLoadPositionUs() {
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
@@ -198,7 +212,25 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
callback.onContinueLoadingRequested(this);
}
- private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) {
+ /* package */ boolean isPendingInitialDiscontinuity() {
+ return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET;
+ }
+
+ private SeekParameters clipSeekParameters(long offsetPositionUs, SeekParameters seekParameters) {
+ long toleranceBeforeMs = Math.min(offsetPositionUs - startUs, seekParameters.toleranceBeforeUs);
+ long toleranceAfterMs =
+ endUs == C.TIME_END_OF_SOURCE
+ ? seekParameters.toleranceAfterUs
+ : Math.min(endUs - offsetPositionUs, seekParameters.toleranceAfterUs);
+ if (toleranceBeforeMs == seekParameters.toleranceBeforeUs
+ && toleranceAfterMs == seekParameters.toleranceAfterUs) {
+ return seekParameters;
+ } else {
+ return new SeekParameters(toleranceBeforeMs, toleranceAfterMs);
+ }
+ }
+
+ private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) {
// If the clipping start position is non-zero, the clipping sample streams will adjust
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
// timestamps can be negative, because sample streams provide buffers starting at a key-frame,
@@ -208,11 +240,13 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
// discontinuity which resets the renderers before they read the clipping sample stream.
// However, for audio-only track selections we assume to have random access seek behaviour and
// do not need an initial discontinuity to reset the renderer.
- for (TrackSelection trackSelection : selections) {
- if (trackSelection != null) {
- Format selectedFormat = trackSelection.getSelectedFormat();
- if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {
- return true;
+ if (startUs != 0) {
+ for (TrackSelection trackSelection : selections) {
+ if (trackSelection != null) {
+ Format selectedFormat = trackSelection.getSelectedFormat();
+ if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) {
+ return true;
+ }
}
}
}
@@ -224,23 +258,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
*/
private final class ClippingSampleStream implements SampleStream {
- private final SampleStream stream;
- private final long startUs;
- private final long endUs;
+ public final SampleStream childStream;
- private boolean pendingDiscontinuity;
private boolean sentEos;
- public ClippingSampleStream(SampleStream stream, long startUs, long endUs,
- boolean pendingDiscontinuity) {
- this.stream = stream;
- this.startUs = startUs;
- this.endUs = endUs;
- this.pendingDiscontinuity = pendingDiscontinuity;
- }
-
- public void clearPendingDiscontinuity() {
- pendingDiscontinuity = false;
+ public ClippingSampleStream(SampleStream childStream) {
+ this.childStream = childStream;
}
public void clearSentEos() {
@@ -249,31 +272,33 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public boolean isReady() {
- return stream.isReady();
+ return !isPendingInitialDiscontinuity() && childStream.isReady();
}
@Override
public void maybeThrowError() throws IOException {
- stream.maybeThrowError();
+ childStream.maybeThrowError();
}
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean requireFormat) {
- if (pendingDiscontinuity) {
+ if (isPendingInitialDiscontinuity()) {
return C.RESULT_NOTHING_READ;
}
if (sentEos) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
- int result = stream.readData(formatHolder, buffer, requireFormat);
+ int result = childStream.readData(formatHolder, buffer, requireFormat);
if (result == C.RESULT_FORMAT_READ) {
- // Clear gapless playback metadata if the start/end points don't match the media.
Format format = formatHolder.format;
- int encoderDelay = startUs != 0 ? 0 : format.encoderDelay;
- int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding;
- formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
+ if (format.encoderDelay != Format.NO_VALUE || format.encoderPadding != Format.NO_VALUE) {
+ // Clear gapless playback metadata if the start/end points don't match the media.
+ int encoderDelay = startUs != 0 ? 0 : format.encoderDelay;
+ int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding;
+ formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding);
+ }
return C.RESULT_FORMAT_READ;
}
if (endUs != C.TIME_END_OF_SOURCE
@@ -293,7 +318,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public int skipData(long positionUs) {
- return stream.skipData(startUs + positionUs);
+ if (isPendingInitialDiscontinuity()) {
+ return C.RESULT_NOTHING_READ;
+ }
+ return childStream.skipData(startUs + positionUs);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
index c6924e844a..9ff704e75a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java
@@ -15,20 +15,68 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
/**
* {@link MediaSource} that wraps a source and clips its timeline based on specified start/end
- * positions. The wrapped source may only have a single period/window and it must not be dynamic
- * (live).
+ * positions. The wrapped source must consist of a single period that starts at the beginning of the
+ * corresponding window.
*/
-public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
+public final class ClippingMediaSource extends CompositeMediaSource {
+
+ /**
+ * Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source.
+ */
+ public static final class IllegalClippingException extends IOException {
+
+ /**
+ * The reason the clipping failed.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW,
+ REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END})
+ public @interface Reason {}
+ /**
+ * The wrapped source doesn't consist of a single period.
+ */
+ public static final int REASON_INVALID_PERIOD_COUNT = 0;
+ /**
+ * The wrapped source period doesn't start at the beginning of the corresponding window.
+ */
+ public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1;
+ /**
+ * The wrapped source is not seekable and a non-zero clipping start position was specified.
+ */
+ public static final int REASON_NOT_SEEKABLE_TO_START = 2;
+ /**
+ * The wrapped source ends before the specified clipping start position.
+ */
+ public static final int REASON_START_EXCEEDS_END = 3;
+
+ /**
+ * The reason clipping failed.
+ */
+ @Reason
+ public final int reason;
+
+ /**
+ * @param reason The reason clipping failed.
+ */
+ public IllegalClippingException(@Reason int reason) {
+ this.reason = reason;
+ }
+
+ }
private final MediaSource mediaSource;
private final long startUs;
@@ -37,11 +85,12 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final ArrayList mediaPeriods;
private MediaSource.Listener sourceListener;
+ private IllegalClippingException clippingError;
/**
* Creates a new clipping source that wraps the specified source.
*
- * @param mediaSource The single-period, non-dynamic source to wrap.
+ * @param mediaSource The single-period source to wrap.
* @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
* start providing samples, in microseconds.
* @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
@@ -61,7 +110,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period
* is first read from.
*
- * @param mediaSource The single-period, non-dynamic source to wrap.
+ * @param mediaSource The single-period source to wrap.
* @param startPositionUs The start position within {@code mediaSource}'s timeline at which to
* start providing samples, in microseconds.
* @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop
@@ -83,13 +132,17 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
- this.sourceListener = listener;
- mediaSource.prepareSource(player, false, this);
+ super.prepareSource(player, isTopLevelSource, listener);
+ sourceListener = listener;
+ prepareChildSource(/* id= */ null, mediaSource);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
- mediaSource.maybeThrowSourceInfoRefreshError();
+ if (clippingError != null) {
+ throw clippingError;
+ }
+ super.maybeThrowSourceInfoRefreshError();
}
@Override
@@ -109,15 +162,25 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override
public void releaseSource() {
- mediaSource.releaseSource();
+ super.releaseSource();
+ clippingError = null;
+ sourceListener = null;
}
- // MediaSource.Listener implementation.
-
@Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- sourceListener.onSourceInfoRefreshed(this, new ClippingTimeline(timeline, startUs, endUs),
- manifest);
+ protected void onChildSourceInfoRefreshed(
+ Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
+ if (clippingError != null) {
+ return;
+ }
+ ClippingTimeline clippingTimeline;
+ try {
+ clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
+ } catch (IllegalClippingException e) {
+ clippingError = e;
+ return;
+ }
+ sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest);
int count = mediaPeriods.size();
for (int i = 0; i < count; i++) {
mediaPeriods.get(i).setClipping(startUs, endUs);
@@ -139,23 +202,30 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
* @param startUs The number of microseconds to clip from the start of {@code timeline}.
* @param endUs The end position in microseconds for the clipped timeline relative to the start
* of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end.
+ * @throws IllegalClippingException If the timeline could not be clipped.
*/
- public ClippingTimeline(Timeline timeline, long startUs, long endUs) {
+ public ClippingTimeline(Timeline timeline, long startUs, long endUs)
+ throws IllegalClippingException {
super(timeline);
- Assertions.checkArgument(timeline.getWindowCount() == 1);
- Assertions.checkArgument(timeline.getPeriodCount() == 1);
+ if (timeline.getPeriodCount() != 1) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT);
+ }
+ if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW);
+ }
Window window = timeline.getWindow(0, new Window(), false);
- Assertions.checkArgument(!window.isDynamic);
long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs;
if (window.durationUs != C.TIME_UNSET) {
if (resolvedEndUs > window.durationUs) {
resolvedEndUs = window.durationUs;
}
- Assertions.checkArgument(startUs == 0 || window.isSeekable);
- Assertions.checkArgument(startUs <= resolvedEndUs);
+ if (startUs != 0 && !window.isSeekable) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START);
+ }
+ if (startUs > resolvedEndUs) {
+ throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END);
+ }
}
- Period period = timeline.getPeriod(0, new Period());
- Assertions.checkArgument(period.getPositionInWindowUs() == 0);
this.startUs = startUs;
this.endUs = resolvedEndUs;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
new file mode 100644
index 0000000000..6472fe3c2f
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeMediaSource.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 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 android.support.annotation.CallSuper;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.util.Assertions;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Composite {@link MediaSource} consisting of multiple child sources.
+ *
+ * @param The type of the id used to identify prepared child sources.
+ */
+public abstract class CompositeMediaSource implements MediaSource {
+
+ private final HashMap childSources;
+ private ExoPlayer player;
+
+ /** Create composite media source without child sources. */
+ protected CompositeMediaSource() {
+ childSources = new HashMap<>();
+ }
+
+ @Override
+ @CallSuper
+ public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ this.player = player;
+ }
+
+ @Override
+ @CallSuper
+ public void maybeThrowSourceInfoRefreshError() throws IOException {
+ for (MediaSource childSource : childSources.values()) {
+ childSource.maybeThrowSourceInfoRefreshError();
+ }
+ }
+
+ @Override
+ @CallSuper
+ public void releaseSource() {
+ for (MediaSource childSource : childSources.values()) {
+ childSource.releaseSource();
+ }
+ childSources.clear();
+ player = null;
+ }
+
+ /**
+ * Called when the source info of a child source has been refreshed.
+ *
+ * @param id The unique id used to prepare the child source.
+ * @param mediaSource The child source whose source info has been refreshed.
+ * @param timeline The timeline of the child source.
+ * @param manifest The manifest of the child source.
+ */
+ protected abstract void onChildSourceInfoRefreshed(
+ @Nullable T id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest);
+
+ /**
+ * Prepares a child source.
+ *
+ * {@link #onChildSourceInfoRefreshed(Object, MediaSource, Timeline, Object)} will be called
+ * when the child source updates its timeline and/or manifest with the same {@code id} passed to
+ * this method.
+ *
+ *
Any child sources that aren't explicitly released with {@link #releaseChildSource(Object)}
+ * will be released in {@link #releaseSource()}.
+ *
+ * @param id A unique id to identify the child source preparation. Null is allowed as an id.
+ * @param mediaSource The child {@link MediaSource}.
+ */
+ protected void prepareChildSource(@Nullable final T id, final MediaSource mediaSource) {
+ Assertions.checkArgument(!childSources.containsKey(id));
+ childSources.put(id, mediaSource);
+ mediaSource.prepareSource(
+ player,
+ /* isTopLevelSource= */ false,
+ new Listener() {
+ @Override
+ public void onSourceInfoRefreshed(
+ MediaSource source, Timeline timeline, @Nullable Object manifest) {
+ onChildSourceInfoRefreshed(id, mediaSource, timeline, manifest);
+ }
+ });
+ }
+
+ /**
+ * Releases a child source.
+ *
+ * @param id The unique id used to prepare the child source.
+ */
+ protected void releaseChildSource(@Nullable T id) {
+ MediaSource removedChild = childSources.remove(id);
+ removedChild.releaseSource();
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
index a85d589762..c41933b48b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java
@@ -20,9 +20,9 @@ import com.google.android.exoplayer2.C;
/**
* A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s.
*/
-public final class CompositeSequenceableLoader implements SequenceableLoader {
+public class CompositeSequenceableLoader implements SequenceableLoader {
- private final SequenceableLoader[] loaders;
+ protected final SequenceableLoader[] loaders;
public CompositeSequenceableLoader(SequenceableLoader[] loaders) {
this.loaders = loaders;
@@ -53,7 +53,14 @@ public final class CompositeSequenceableLoader implements SequenceableLoader {
}
@Override
- public final boolean continueLoading(long positionUs) {
+ public final void reevaluateBuffer(long positionUs) {
+ for (SequenceableLoader loader : loaders) {
+ loader.reevaluateBuffer(positionUs);
+ }
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
boolean madeProgress = false;
boolean madeProgressThisIteration;
do {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java
new file mode 100644
index 0000000000..b4a266feef
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * A factory to create composite {@link SequenceableLoader}s.
+ */
+public interface CompositeSequenceableLoaderFactory {
+
+ /**
+ * Creates a composite {@link SequenceableLoader}.
+ *
+ * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built.
+ * @return A composite {@link SequenceableLoader} that comprises the given loaders.
+ */
+ SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders);
+
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
index 058471f31f..c29367e109 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java
@@ -15,15 +15,14 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
-import java.io.IOException;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
@@ -32,13 +31,12 @@ import java.util.Map;
* Concatenates multiple {@link MediaSource}s. It is valid for the same {@link MediaSource} instance
* to be present more than once in the concatenation.
*/
-public final class ConcatenatingMediaSource implements MediaSource {
+public final class ConcatenatingMediaSource extends CompositeMediaSource {
private final MediaSource[] mediaSources;
private final Timeline[] timelines;
private final Object[] manifests;
private final Map sourceIndexByMediaPeriod;
- private final boolean[] duplicateFlags;
private final boolean isAtomic;
private final ShuffleOrder shuffleOrder;
@@ -84,39 +82,24 @@ public final class ConcatenatingMediaSource implements MediaSource {
timelines = new Timeline[mediaSources.length];
manifests = new Object[mediaSources.length];
sourceIndexByMediaPeriod = new HashMap<>();
- duplicateFlags = buildDuplicateFlags(mediaSources);
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ super.prepareSource(player, isTopLevelSource, listener);
this.listener = listener;
+ boolean[] duplicateFlags = buildDuplicateFlags(mediaSources);
if (mediaSources.length == 0) {
listener.onSourceInfoRefreshed(this, Timeline.EMPTY, null);
} else {
for (int i = 0; i < mediaSources.length; i++) {
if (!duplicateFlags[i]) {
- final int index = i;
- mediaSources[i].prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
- Object manifest) {
- handleSourceInfoRefreshed(index, timeline, manifest);
- }
- });
+ prepareChildSource(i, mediaSources[i]);
}
}
}
}
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- for (int i = 0; i < mediaSources.length; i++) {
- if (!duplicateFlags[i]) {
- mediaSources[i].maybeThrowSourceInfoRefreshError();
- }
- }
- }
-
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex);
@@ -136,21 +119,23 @@ public final class ConcatenatingMediaSource implements MediaSource {
@Override
public void releaseSource() {
- for (int i = 0; i < mediaSources.length; i++) {
- if (!duplicateFlags[i]) {
- mediaSources[i].releaseSource();
- }
- }
+ super.releaseSource();
+ listener = null;
+ timeline = null;
}
- private void handleSourceInfoRefreshed(int sourceFirstIndex, Timeline sourceTimeline,
- Object sourceManifest) {
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Integer sourceFirstIndex,
+ MediaSource mediaSource,
+ Timeline sourceTimeline,
+ @Nullable Object sourceManifest) {
// Set the timeline and manifest.
timelines[sourceFirstIndex] = sourceTimeline;
manifests[sourceFirstIndex] = sourceManifest;
// Also set the timeline and manifest for any duplicate entries of the same source.
for (int i = sourceFirstIndex + 1; i < mediaSources.length; i++) {
- if (mediaSources[i] == mediaSources[sourceFirstIndex]) {
+ if (mediaSources[i] == mediaSource) {
timelines[i] = sourceTimeline;
manifests[i] = sourceManifest;
}
@@ -187,10 +172,9 @@ public final class ConcatenatingMediaSource implements MediaSource {
private final Timeline[] timelines;
private final int[] sourcePeriodOffsets;
private final int[] sourceWindowOffsets;
- private final boolean isAtomic;
public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) {
- super(shuffleOrder);
+ super(isAtomic, shuffleOrder);
int[] sourcePeriodOffsets = new int[timelines.length];
int[] sourceWindowOffsets = new int[timelines.length];
long periodCount = 0;
@@ -207,7 +191,6 @@ public final class ConcatenatingMediaSource implements MediaSource {
this.timelines = timelines;
this.sourcePeriodOffsets = sourcePeriodOffsets;
this.sourceWindowOffsets = sourceWindowOffsets;
- this.isAtomic = isAtomic;
}
@Override
@@ -220,34 +203,6 @@ public final class ConcatenatingMediaSource implements MediaSource {
return sourcePeriodOffsets[sourcePeriodOffsets.length - 1];
}
- @Override
- public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
- boolean shuffleModeEnabled) {
- if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
- repeatMode = Player.REPEAT_MODE_ALL;
- }
- return super.getNextWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled);
- }
-
- @Override
- public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode,
- boolean shuffleModeEnabled) {
- if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) {
- repeatMode = Player.REPEAT_MODE_ALL;
- }
- return super.getPreviousWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled);
- }
-
- @Override
- public int getLastWindowIndex(boolean shuffleModeEnabled) {
- return super.getLastWindowIndex(!isAtomic && shuffleModeEnabled);
- }
-
- @Override
- public int getFirstWindowIndex(boolean shuffleModeEnabled) {
- return super.getFirstWindowIndex(!isAtomic && shuffleModeEnabled);
- }
-
@Override
protected int getChildIndexByPeriodIndex(int periodIndex) {
return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
similarity index 52%
rename from library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java
rename to library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
index 4cc32bb9e4..759b0824af 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaOutputBuffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -13,28 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.google.android.exoplayer2.text.cea;
-
-import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
+package com.google.android.exoplayer2.source;
/**
- * A {@link SubtitleOutputBuffer} for {@link CeaDecoder}s.
+ * Default implementation of {@link CompositeSequenceableLoaderFactory}.
*/
-public final class CeaOutputBuffer extends SubtitleOutputBuffer {
-
- private final CeaDecoder owner;
-
- /**
- * @param owner The decoder that owns this buffer.
- */
- public CeaOutputBuffer(CeaDecoder owner) {
- super();
- this.owner = owner;
- }
+public final class DefaultCompositeSequenceableLoaderFactory
+ implements CompositeSequenceableLoaderFactory {
@Override
- public final void release() {
- owner.releaseOutputBuffer(this);
+ public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) {
+ return new CompositeSequenceableLoader(loaders);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
index f93d30cb04..e13a563d50 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java
@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.Nullable;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -28,6 +30,15 @@ import java.io.IOException;
*/
public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+ /** Listener for preparation errors. */
+ public interface PrepareErrorListener {
+
+ /**
+ * Called the first time an error occurs while refreshing source info or preparing the period.
+ */
+ void onPrepareError(IOException exception);
+ }
+
public final MediaSource mediaSource;
private final MediaPeriodId id;
@@ -36,13 +47,33 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
private MediaPeriod mediaPeriod;
private Callback callback;
private long preparePositionUs;
+ private @Nullable PrepareErrorListener listener;
+ private boolean notifiedPrepareError;
+ /**
+ * Creates a new deferred media period.
+ *
+ * @param mediaSource The media source to wrap.
+ * @param id The identifier for the media period to create when {@link #createPeriod()} is called.
+ * @param allocator The allocator used to create the media period.
+ */
public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) {
this.id = id;
this.allocator = allocator;
this.mediaSource = mediaSource;
}
+ /**
+ * Sets a listener for preparation errors.
+ *
+ * @param listener An listener to be notified of media period preparation errors. If a listener is
+ * set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first
+ * preparation error (if any) to the listener.
+ */
+ public void setPrepareErrorListener(PrepareErrorListener listener) {
+ this.listener = listener;
+ }
+
/**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then
* prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()}
@@ -75,10 +106,20 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
@Override
public void maybeThrowPrepareError() throws IOException {
- if (mediaPeriod != null) {
- mediaPeriod.maybeThrowPrepareError();
- } else {
- mediaSource.maybeThrowSourceInfoRefreshError();
+ try {
+ if (mediaPeriod != null) {
+ mediaPeriod.maybeThrowPrepareError();
+ } else {
+ mediaSource.maybeThrowSourceInfoRefreshError();
+ }
+ } catch (final IOException e) {
+ if (listener == null) {
+ throw e;
+ }
+ if (!notifiedPrepareError) {
+ notifiedPrepareError = true;
+ listener.onPrepareError(e);
+ }
}
}
@@ -95,8 +136,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
}
@Override
- public void discardBuffer(long positionUs) {
- mediaPeriod.discardBuffer(positionUs);
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ mediaPeriod.discardBuffer(positionUs, toKeyframe);
}
@Override
@@ -114,11 +155,21 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb
return mediaPeriod.seekToUs(positionUs);
}
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
@Override
public long getNextLoadPositionUs() {
return mediaPeriod.getNextLoadPositionUs();
}
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ mediaPeriod.reevaluateBuffer(positionUs);
+ }
+
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
index b66e5ebe09..f52c1bfd0f 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java
@@ -23,14 +23,13 @@ import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent;
-import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
+import com.google.android.exoplayer2.PlayerMessage;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource.MediaSourceHolder;
import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -42,7 +41,8 @@ import java.util.Map;
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
* during playback. Access to this class is thread-safe.
*/
-public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent {
+public final class DynamicConcatenatingMediaSource extends CompositeMediaSource
+ implements PlayerMessage.Target {
private static final int MSG_ADD = 0;
private static final int MSG_ADD_MULTIPLE = 1;
@@ -56,8 +56,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
// Accessed on the playback thread.
private final List mediaSourceHolders;
private final MediaSourceHolder query;
- private final Map mediaSourceByMediaPeriod;
+ private final Map mediaSourceByMediaPeriod;
private final List deferredMediaPeriods;
+ private final boolean isAtomic;
private ExoPlayer player;
private Listener listener;
@@ -70,22 +71,35 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
* Creates a new dynamic concatenating media source.
*/
public DynamicConcatenatingMediaSource() {
- this(new DefaultShuffleOrder(0));
+ this(/* isAtomic= */ false, new DefaultShuffleOrder(0));
+ }
+
+ /**
+ * Creates a new dynamic concatenating media source.
+ *
+ * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
+ * as a single item for repeating and shuffling.
+ */
+ public DynamicConcatenatingMediaSource(boolean isAtomic) {
+ this(isAtomic, new DefaultShuffleOrder(0));
}
/**
* Creates a new dynamic concatenating media source with a custom shuffle order.
*
+ * @param isAtomic Whether the concatenating media source will be treated as atomic, i.e., treated
+ * as a single item for repeating and shuffling.
* @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources.
* This shuffle order must be empty.
*/
- public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) {
+ public DynamicConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder) {
this.shuffleOrder = shuffleOrder;
this.mediaSourceByMediaPeriod = new IdentityHashMap<>();
this.mediaSourcesPublic = new ArrayList<>();
this.mediaSourceHolders = new ArrayList<>();
this.deferredMediaPeriods = new ArrayList<>(1);
this.query = new MediaSourceHolder(null, null, -1, -1, -1);
+ this.isAtomic = isAtomic;
}
/**
@@ -147,8 +161,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource));
mediaSourcesPublic.add(index, mediaSource);
if (player != null) {
- player.sendMessages(new ExoPlayerMessage(this, MSG_ADD,
- new MessageData<>(index, mediaSource, actionOnCompletion)));
+ player
+ .createMessage(this)
+ .setType(MSG_ADD)
+ .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion))
+ .send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
@@ -220,8 +237,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
}
mediaSourcesPublic.addAll(index, mediaSources);
if (player != null && !mediaSources.isEmpty()) {
- player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE,
- new MessageData<>(index, mediaSources, actionOnCompletion)));
+ player
+ .createMessage(this)
+ .setType(MSG_ADD_MULTIPLE)
+ .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion))
+ .send();
} else if (actionOnCompletion != null){
actionOnCompletion.run();
}
@@ -256,8 +276,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) {
mediaSourcesPublic.remove(index);
if (player != null) {
- player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE,
- new MessageData<>(index, null, actionOnCompletion)));
+ player
+ .createMessage(this)
+ .setType(MSG_REMOVE)
+ .setPayload(new MessageData<>(index, null, actionOnCompletion))
+ .send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
@@ -293,8 +316,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
}
mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex));
if (player != null) {
- player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE,
- new MessageData<>(currentIndex, newIndex, actionOnCompletion)));
+ player
+ .createMessage(this)
+ .setType(MSG_MOVE)
+ .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion))
+ .send();
} else if (actionOnCompletion != null) {
actionOnCompletion.run();
}
@@ -320,6 +346,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override
public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource,
Listener listener) {
+ super.prepareSource(player, isTopLevelSource, listener);
this.player = player;
this.listener = listener;
preventListenerNotification = true;
@@ -329,13 +356,6 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
maybeNotifyListener(null);
}
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- for (int i = 0; i < mediaSourceHolders.size(); i++) {
- mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError();
- }
- }
-
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
int mediaSourceHolderIndex = findMediaSourceHolderByPeriodIndex(id.periodIndex);
@@ -349,27 +369,44 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
} else {
mediaPeriod = holder.mediaSource.createPeriod(idInSource, allocator);
}
- mediaSourceByMediaPeriod.put(mediaPeriod, holder.mediaSource);
+ mediaSourceByMediaPeriod.put(mediaPeriod, holder);
+ holder.activeMediaPeriods++;
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
- MediaSource mediaSource = mediaSourceByMediaPeriod.get(mediaPeriod);
- mediaSourceByMediaPeriod.remove(mediaPeriod);
+ MediaSourceHolder holder = mediaSourceByMediaPeriod.remove(mediaPeriod);
if (mediaPeriod instanceof DeferredMediaPeriod) {
deferredMediaPeriods.remove(mediaPeriod);
((DeferredMediaPeriod) mediaPeriod).releasePeriod();
} else {
- mediaSource.releasePeriod(mediaPeriod);
+ holder.mediaSource.releasePeriod(mediaPeriod);
+ }
+ holder.activeMediaPeriods--;
+ if (holder.activeMediaPeriods == 0 && holder.isRemoved) {
+ releaseChildSource(holder);
}
}
@Override
public void releaseSource() {
- for (int i = 0; i < mediaSourceHolders.size(); i++) {
- mediaSourceHolders.get(i).mediaSource.releaseSource();
- }
+ super.releaseSource();
+ mediaSourceHolders.clear();
+ player = null;
+ listener = null;
+ shuffleOrder = shuffleOrder.cloneAndClear();
+ windowCount = 0;
+ periodCount = 0;
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ MediaSourceHolder mediaSourceHolder,
+ MediaSource mediaSource,
+ Timeline timeline,
+ @Nullable Object manifest) {
+ updateMediaSourceInternal(mediaSourceHolder, timeline);
}
@Override
@@ -423,37 +460,39 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
private void maybeNotifyListener(@Nullable EventDispatcher actionOnCompletion) {
if (!preventListenerNotification) {
- listener.onSourceInfoRefreshed(this,
- new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder),
+ listener.onSourceInfoRefreshed(
+ this,
+ new ConcatenatedTimeline(
+ mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic),
null);
if (actionOnCompletion != null) {
- player.sendMessages(
- new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion));
+ player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send();
}
}
}
private void addMediaSourceInternal(int newIndex, MediaSource newMediaSource) {
final MediaSourceHolder newMediaSourceHolder;
- Object newUid = System.identityHashCode(newMediaSource);
DeferredTimeline newTimeline = new DeferredTimeline();
if (newIndex > 0) {
MediaSourceHolder previousHolder = mediaSourceHolders.get(newIndex - 1);
- newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline,
- previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
- previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount(),
- newUid);
+ newMediaSourceHolder =
+ new MediaSourceHolder(
+ newMediaSource,
+ newTimeline,
+ newIndex,
+ previousHolder.firstWindowIndexInChild + previousHolder.timeline.getWindowCount(),
+ previousHolder.firstPeriodIndexInChild + previousHolder.timeline.getPeriodCount());
} else {
- newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, newUid);
+ newMediaSourceHolder = new MediaSourceHolder(newMediaSource, newTimeline, 0, 0, 0);
}
- correctOffsets(newIndex, newTimeline.getWindowCount(), newTimeline.getPeriodCount());
+ correctOffsets(
+ newIndex,
+ /* childIndexUpdate= */ 1,
+ newTimeline.getWindowCount(),
+ newTimeline.getPeriodCount());
mediaSourceHolders.add(newIndex, newMediaSourceHolder);
- newMediaSourceHolder.mediaSource.prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) {
- updateMediaSourceInternal(newMediaSourceHolder, newTimeline);
- }
- });
+ prepareChildSource(newMediaSourceHolder, newMediaSourceHolder.mediaSource);
}
private void addMediaSourcesInternal(int index, Collection mediaSources) {
@@ -473,8 +512,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
int windowOffsetUpdate = timeline.getWindowCount() - deferredTimeline.getWindowCount();
int periodOffsetUpdate = timeline.getPeriodCount() - deferredTimeline.getPeriodCount();
if (windowOffsetUpdate != 0 || periodOffsetUpdate != 0) {
- int index = findMediaSourceHolderByPeriodIndex(mediaSourceHolder.firstPeriodIndexInChild);
- correctOffsets(index + 1, windowOffsetUpdate, periodOffsetUpdate);
+ correctOffsets(
+ mediaSourceHolder.childIndex + 1,
+ /* childIndexUpdate= */ 0,
+ windowOffsetUpdate,
+ periodOffsetUpdate);
}
mediaSourceHolder.timeline = deferredTimeline.cloneWithNewTimeline(timeline);
if (!mediaSourceHolder.isPrepared) {
@@ -493,8 +535,15 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
MediaSourceHolder holder = mediaSourceHolders.get(index);
mediaSourceHolders.remove(index);
Timeline oldTimeline = holder.timeline;
- correctOffsets(index, -oldTimeline.getWindowCount(), -oldTimeline.getPeriodCount());
- holder.mediaSource.releaseSource();
+ correctOffsets(
+ index,
+ /* childIndexUpdate= */ -1,
+ -oldTimeline.getWindowCount(),
+ -oldTimeline.getPeriodCount());
+ holder.isRemoved = true;
+ if (holder.activeMediaPeriods == 0) {
+ releaseChildSource(holder);
+ }
}
private void moveMediaSourceInternal(int currentIndex, int newIndex) {
@@ -512,10 +561,12 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
}
}
- private void correctOffsets(int startIndex, int windowOffsetUpdate, int periodOffsetUpdate) {
+ private void correctOffsets(
+ int startIndex, int childIndexUpdate, int windowOffsetUpdate, int periodOffsetUpdate) {
windowCount += windowOffsetUpdate;
periodCount += periodOffsetUpdate;
for (int i = startIndex; i < mediaSourceHolders.size(); i++) {
+ mediaSourceHolders.get(i).childIndex += childIndexUpdate;
mediaSourceHolders.get(i).firstWindowIndexInChild += windowOffsetUpdate;
mediaSourceHolders.get(i).firstPeriodIndexInChild += periodOffsetUpdate;
}
@@ -534,26 +585,32 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
return index;
}
- /**
- * Data class to hold playlist media sources together with meta data needed to process them.
- */
- private static final class MediaSourceHolder implements Comparable {
+ /** Data class to hold playlist media sources together with meta data needed to process them. */
+ /* package */ static final class MediaSourceHolder implements Comparable {
public final MediaSource mediaSource;
- public final Object uid;
+ public final int uid;
public DeferredTimeline timeline;
+ public int childIndex;
public int firstWindowIndexInChild;
public int firstPeriodIndexInChild;
public boolean isPrepared;
+ public boolean isRemoved;
+ public int activeMediaPeriods;
- public MediaSourceHolder(MediaSource mediaSource, DeferredTimeline timeline, int window,
- int period, Object uid) {
+ public MediaSourceHolder(
+ MediaSource mediaSource,
+ DeferredTimeline timeline,
+ int childIndex,
+ int window,
+ int period) {
this.mediaSource = mediaSource;
this.timeline = timeline;
+ this.childIndex = childIndex;
this.firstWindowIndexInChild = window;
this.firstPeriodIndexInChild = period;
- this.uid = uid;
+ this.uid = System.identityHashCode(this);
}
@Override
@@ -582,16 +639,14 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
}
- /**
- * Message used to post actions from app thread to playback thread.
- */
- private static final class MessageData {
+ /** Message used to post actions from app thread to playback thread. */
+ private static final class MessageData {
public final int index;
- public final CustomType customData;
+ public final T customData;
public final @Nullable EventDispatcher actionOnCompletion;
- public MessageData(int index, CustomType customData, @Nullable Runnable actionOnCompletion) {
+ public MessageData(int index, T customData, @Nullable Runnable actionOnCompletion) {
this.index = index;
this.actionOnCompletion = actionOnCompletion != null
? new EventDispatcher(actionOnCompletion) : null;
@@ -613,9 +668,13 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
private final int[] uids;
private final SparseIntArray childIndexByUid;
- public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount,
- int periodCount, ShuffleOrder shuffleOrder) {
- super(shuffleOrder);
+ public ConcatenatedTimeline(
+ Collection mediaSourceHolders,
+ int windowCount,
+ int periodCount,
+ ShuffleOrder shuffleOrder,
+ boolean isAtomic) {
+ super(isAtomic, shuffleOrder);
this.windowCount = windowCount;
this.periodCount = periodCount;
int childCount = mediaSourceHolders.size();
@@ -629,7 +688,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
timelines[index] = mediaSourceHolder.timeline;
firstPeriodInChildIndices[index] = mediaSourceHolder.firstPeriodIndexInChild;
firstWindowInChildIndices[index] = mediaSourceHolder.firstWindowIndexInChild;
- uids[index] = (int) mediaSourceHolder.uid;
+ uids[index] = mediaSourceHolder.uid;
childIndexByUid.put(uids[index], index++);
}
}
@@ -689,61 +748,39 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
* Timeline used as placeholder for an unprepared media source. After preparation, a copy of the
* DeferredTimeline is used to keep the originally assigned first period ID.
*/
- private static final class DeferredTimeline extends Timeline {
+ private static final class DeferredTimeline extends ForwardingTimeline {
private static final Object DUMMY_ID = new Object();
private static final Period period = new Period();
+ private static final DummyTimeline dummyTimeline = new DummyTimeline();
- private final Timeline timeline;
- private final Object replacedID;
+ private final Object replacedId;
public DeferredTimeline() {
- timeline = null;
- replacedID = null;
+ this(dummyTimeline, /* replacedId= */ null);
}
- private DeferredTimeline(Timeline timeline, Object replacedID) {
- this.timeline = timeline;
- this.replacedID = replacedID;
+ private DeferredTimeline(Timeline timeline, Object replacedId) {
+ super(timeline);
+ this.replacedId = replacedId;
}
public DeferredTimeline cloneWithNewTimeline(Timeline timeline) {
- return new DeferredTimeline(timeline, replacedID == null && timeline.getPeriodCount() > 0
- ? timeline.getPeriod(0, period, true).uid : replacedID);
+ return new DeferredTimeline(
+ timeline,
+ replacedId == null && timeline.getPeriodCount() > 0
+ ? timeline.getPeriod(0, period, true).uid
+ : replacedId);
}
public Timeline getTimeline() {
return timeline;
}
- @Override
- public int getWindowCount() {
- return timeline == null ? 1 : timeline.getWindowCount();
- }
-
- @Override
- public Window getWindow(int windowIndex, Window window, boolean setIds,
- long defaultPositionProjectionUs) {
- return timeline == null
- // Dynamic window to indicate pending timeline updates.
- ? window.set(setIds ? DUMMY_ID : null, C.TIME_UNSET, C.TIME_UNSET, false, true, 0,
- C.TIME_UNSET, 0, 0, 0)
- : timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
- }
-
- @Override
- public int getPeriodCount() {
- return timeline == null ? 1 : timeline.getPeriodCount();
- }
-
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
- if (timeline == null) {
- return period.set(setIds ? DUMMY_ID : null, setIds ? DUMMY_ID : null, 0, C.TIME_UNSET,
- C.TIME_UNSET);
- }
timeline.getPeriod(periodIndex, period, setIds);
- if (period.uid == replacedID) {
+ if (Util.areEqual(period.uid, replacedId)) {
period.uid = DUMMY_ID;
}
return period;
@@ -751,10 +788,54 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl
@Override
public int getIndexOfPeriod(Object uid) {
- return timeline == null ? (uid == DUMMY_ID ? 0 : C.INDEX_UNSET)
- : timeline.getIndexOfPeriod(uid == DUMMY_ID ? replacedID : uid);
+ return timeline.getIndexOfPeriod(DUMMY_ID.equals(uid) ? replacedId : uid);
}
-
}
+ /** Dummy placeholder timeline with one dynamic window with a period of indeterminate duration. */
+ private static final class DummyTimeline extends Timeline {
+
+ @Override
+ public int getWindowCount() {
+ return 1;
+ }
+
+ @Override
+ public Window getWindow(int windowIndex, Window window, boolean setIds,
+ long defaultPositionProjectionUs) {
+ // Dynamic window to indicate pending timeline updates.
+ return window.set(
+ /* id= */ null,
+ /* presentationStartTimeMs= */ C.TIME_UNSET,
+ /* windowStartTimeMs= */ C.TIME_UNSET,
+ /* isSeekable= */ false,
+ /* isDynamic= */ true,
+ /* defaultPositionUs= */ 0,
+ /* durationUs= */ C.TIME_UNSET,
+ /* firstPeriodIndex= */ 0,
+ /* lastPeriodIndex= */ 0,
+ /* positionInFirstPeriodUs= */ 0);
+ }
+
+ @Override
+ public int getPeriodCount() {
+ return 1;
+ }
+
+ @Override
+ public Period getPeriod(int periodIndex, Period period, boolean setIds) {
+ return period.set(
+ /* id= */ null,
+ /* uid= */ null,
+ /* windowIndex= */ 0,
+ /* durationUs = */ C.TIME_UNSET,
+ /* positionInWindowUs= */ C.TIME_UNSET);
+ }
+
+ @Override
+ public int getIndexOfPeriod(Object uid) {
+ return uid == null ? 0 : C.INDEX_UNSET;
+ }
+ }
}
+
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
index f907dc6229..c771188e3b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java
@@ -21,6 +21,7 @@ import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.extractor.DefaultExtractorInput;
import com.google.android.exoplayer2.extractor.Extractor;
@@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap;
+import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener;
@@ -103,11 +105,13 @@ import java.util.Arrays;
private long durationUs;
private boolean[] trackEnabledStates;
private boolean[] trackIsAudioVideoFlags;
+ private boolean[] trackFormatNotificationSent;
private boolean haveAudioVideoTracks;
private long length;
private long lastSeekPositionUs;
private long pendingResetPositionUs;
+ private boolean pendingDeferredRetry;
private int extractedSamplesCountAtStartOfLoad;
private boolean loadingFinished;
@@ -166,6 +170,7 @@ import java.util.Arrays;
sampleQueues = new SampleQueue[0];
pendingResetPositionUs = C.TIME_UNSET;
length = C.LENGTH_UNSET;
+ durationUs = C.TIME_UNSET;
// Assume on-demand for MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, until prepared.
actualMinLoadableRetryCount =
minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA
@@ -174,24 +179,24 @@ import java.util.Arrays;
}
public void release() {
- boolean releasedSynchronously = loader.release(this);
- if (prepared && !releasedSynchronously) {
+ if (prepared) {
// Discard as much as we can synchronously. We only do this if we're prepared, since otherwise
// sampleQueues may still be being modified by the loading thread.
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.discardToEnd();
}
}
+ loader.release(this);
handler.removeCallbacksAndMessages(null);
released = true;
}
@Override
public void onLoaderReleased() {
- extractorHolder.release();
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
+ extractorHolder.release();
}
@Override
@@ -255,6 +260,7 @@ import java.util.Arrays;
}
}
if (enabledTrackCount == 0) {
+ pendingDeferredRetry = false;
notifyDiscontinuity = false;
if (loader.isLoading()) {
// Discard as much as we can synchronously.
@@ -281,16 +287,21 @@ import java.util.Arrays;
}
@Override
- public void discardBuffer(long positionUs) {
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
int trackCount = sampleQueues.length;
for (int i = 0; i < trackCount; i++) {
- sampleQueues[i].discardTo(positionUs, false, trackEnabledStates[i]);
+ sampleQueues[i].discardTo(positionUs, toKeyframe, trackEnabledStates[i]);
}
}
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ // Do nothing.
+ }
+
@Override
public boolean continueLoading(long playbackPositionUs) {
- if (loadingFinished || (prepared && enabledTrackCount == 0)) {
+ if (loadingFinished || pendingDeferredRetry || (prepared && enabledTrackCount == 0)) {
return false;
}
boolean continuedLoading = loadCondition.open();
@@ -352,6 +363,7 @@ import java.util.Arrays;
return positionUs;
}
// We were unable to seek within the buffer, so need to reset.
+ pendingDeferredRetry = false;
pendingResetPositionUs = positionUs;
loadingFinished = false;
if (loader.isLoading()) {
@@ -364,6 +376,17 @@ import java.util.Arrays;
return positionUs;
}
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ if (!seekMap.isSeekable()) {
+ // Treat all seeks into non-seekable media as being to t=0.
+ return 0;
+ }
+ SeekPoints seekPoints = seekMap.getSeekPoints(positionUs);
+ return Util.resolveSeekPositionUs(
+ positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs);
+ }
+
// SampleStream methods.
/* package */ boolean isReady(int track) {
@@ -379,8 +402,15 @@ import java.util.Arrays;
if (suppressRead()) {
return C.RESULT_NOTHING_READ;
}
- return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished,
- lastSeekPositionUs);
+ int result =
+ sampleQueues[track].read(
+ formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs);
+ if (result == C.RESULT_BUFFER_READ) {
+ maybeNotifyTrackFormat(track);
+ } else if (result == C.RESULT_NOTHING_READ) {
+ maybeStartDeferredRetry(track);
+ }
+ return result;
}
/* package */ int skipData(int track, long positionUs) {
@@ -388,12 +418,51 @@ import java.util.Arrays;
return 0;
}
SampleQueue sampleQueue = sampleQueues[track];
+ int skipCount;
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- return sampleQueue.advanceToEnd();
+ skipCount = sampleQueue.advanceToEnd();
} else {
- int skipCount = sampleQueue.advanceTo(positionUs, true, true);
- return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
+ skipCount = sampleQueue.advanceTo(positionUs, true, true);
+ if (skipCount == SampleQueue.ADVANCE_FAILED) {
+ skipCount = 0;
+ }
}
+ if (skipCount > 0) {
+ maybeNotifyTrackFormat(track);
+ } else {
+ maybeStartDeferredRetry(track);
+ }
+ return skipCount;
+ }
+
+ private void maybeNotifyTrackFormat(int track) {
+ if (!trackFormatNotificationSent[track]) {
+ Format trackFormat = tracks.get(track).getFormat(0);
+ eventDispatcher.downstreamFormatChanged(
+ MimeTypes.getTrackType(trackFormat.sampleMimeType),
+ trackFormat,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ lastSeekPositionUs);
+ trackFormatNotificationSent[track] = true;
+ }
+ }
+
+ private void maybeStartDeferredRetry(int track) {
+ if (!pendingDeferredRetry
+ || !trackIsAudioVideoFlags[track]
+ || sampleQueues[track].hasNextSample()) {
+ return;
+ }
+ pendingResetPositionUs = 0;
+ pendingDeferredRetry = false;
+ notifyDiscontinuity = true;
+ lastSeekPositionUs = 0;
+ extractedSamplesCountAtStartOfLoad = 0;
+ for (SampleQueue sampleQueue : sampleQueues) {
+ sampleQueue.reset();
+ }
+ callback.onContinueLoadingRequested(this);
}
private boolean suppressRead() {
@@ -418,7 +487,7 @@ import java.util.Arrays;
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
- /* mediaStartTimeUs= */ 0,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
@@ -438,7 +507,7 @@ import java.util.Arrays;
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
- /* mediaStartTimeUs= */ 0,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
@@ -465,7 +534,7 @@ import java.util.Arrays;
/* trackFormat= */ null,
C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
- /* mediaStartTimeUs= */ 0,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
durationUs,
elapsedRealtimeMs,
loadDurationMs,
@@ -478,9 +547,9 @@ import java.util.Arrays;
}
int extractedSamplesCount = getExtractedSamplesCount();
boolean madeProgress = extractedSamplesCount > extractedSamplesCountAtStartOfLoad;
- configureRetry(loadable); // May reset the sample queues.
- extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
- return madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY;
+ return configureRetry(loadable, extractedSamplesCount)
+ ? (madeProgress ? Loader.RETRY_RESET_ERROR_COUNT : Loader.RETRY)
+ : Loader.DONT_RETRY;
}
// ExtractorOutput implementation. Called by the loading thread.
@@ -537,6 +606,7 @@ import java.util.Arrays;
TrackGroup[] trackArray = new TrackGroup[trackCount];
trackIsAudioVideoFlags = new boolean[trackCount];
trackEnabledStates = new boolean[trackCount];
+ trackFormatNotificationSent = new boolean[trackCount];
durationUs = seekMap.getDurationUs();
for (int i = 0; i < trackCount; i++) {
Format trackFormat = sampleQueues[i].getUpstreamFormat();
@@ -572,30 +642,65 @@ import java.util.Arrays;
pendingResetPositionUs = C.TIME_UNSET;
return;
}
- loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs);
+ loadable.setLoadPosition(
+ seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs);
pendingResetPositionUs = C.TIME_UNSET;
}
extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount();
- loader.startLoading(loadable, this, actualMinLoadableRetryCount);
+ long elapsedRealtimeMs = loader.startLoading(loadable, this, actualMinLoadableRetryCount);
+ eventDispatcher.loadStarted(
+ loadable.dataSpec,
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ /* trackFormat= */ null,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ loadable.seekTimeUs,
+ durationUs,
+ elapsedRealtimeMs);
}
- private void configureRetry(ExtractingLoadable loadable) {
+ /**
+ * Called to configure a retry when a load error occurs.
+ *
+ * @param loadable The current loadable for which the error was encountered.
+ * @param currentExtractedSampleCount The current number of samples that have been extracted into
+ * the sample queues.
+ * @return Whether the loader should retry with the current loadable. False indicates a deferred
+ * retry.
+ */
+ private boolean configureRetry(ExtractingLoadable loadable, int currentExtractedSampleCount) {
if (length != C.LENGTH_UNSET
|| (seekMap != null && seekMap.getDurationUs() != C.TIME_UNSET)) {
// We're playing an on-demand stream. Resume the current loadable, which will
// request data starting from the point it left off.
+ extractedSamplesCountAtStartOfLoad = currentExtractedSampleCount;
+ return true;
+ } else if (prepared && !suppressRead()) {
+ // We're playing a stream of unknown length and duration. Assume it's live, and therefore that
+ // the data at the uri is a continuously shifting window of the latest available media. For
+ // this case there's no way to continue loading from where a previous load finished, so it's
+ // necessary to load from the start whenever commencing a new load. Deferring the retry until
+ // we run out of buffered data makes for a much better user experience. See:
+ // https://github.com/google/ExoPlayer/issues/1606.
+ // Note that the suppressRead() check means only a single deferred retry can occur without
+ // progress being made. Any subsequent failures without progress will go through the else
+ // block below.
+ pendingDeferredRetry = true;
+ return false;
} else {
- // We're playing a stream of unknown length and duration. Assume it's live, and
- // therefore that the data at the uri is a continuously shifting window of the latest
- // available media. For this case there's no way to continue loading from where a
- // previous load finished, so it's necessary to load from the start whenever commencing
- // a new load.
- lastSeekPositionUs = 0;
+ // This is the same case as above, except in this case there's no value in deferring the retry
+ // because there's no buffered data to be read. This case also covers an on-demand stream with
+ // unknown length that has yet to be prepared. This case cannot be disambiguated from the live
+ // stream case, so we have no option but to load from the start.
notifyDiscontinuity = prepared;
+ lastSeekPositionUs = 0;
+ extractedSamplesCountAtStartOfLoad = 0;
for (SampleQueue sampleQueue : sampleQueues) {
sampleQueue.reset();
}
loadable.setLoadPosition(0, 0);
+ return true;
}
}
@@ -619,7 +724,6 @@ import java.util.Arrays;
if (!seekInsideQueue && (trackIsAudioVideoFlags[i] || !haveAudioVideoTracks)) {
return false;
}
- sampleQueue.discardToRead();
}
return true;
}
@@ -645,7 +749,7 @@ import java.util.Arrays;
return pendingResetPositionUs != C.TIME_UNSET;
}
- private boolean isLoadableExceptionFatal(IOException e) {
+ private static boolean isLoadableExceptionFatal(IOException e) {
return e instanceof UnrecognizedInputFormatException;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
index 3b650482f5..14453653af 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java
@@ -377,8 +377,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe
private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) {
timelineDurationUs = durationUs;
timelineIsSeekable = isSeekable;
- sourceListener.onSourceInfoRefreshed(
- this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null);
+ // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223.
+ sourceListener.onSourceInfoRefreshed(this,
+ new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null);
}
/**
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
index 984820cc6a..e2ef4eb5fa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
@@ -22,20 +23,20 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions;
-import java.io.IOException;
/**
* Loops a {@link MediaSource} a specified number of times.
- *
- * Note: To loop a {@link MediaSource} indefinitely, it is usually better to use
- * {@link ExoPlayer#setRepeatMode(int)}.
+ *
+ *
Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link
+ * ExoPlayer#setRepeatMode(int)}.
*/
-public final class LoopingMediaSource implements MediaSource {
+public final class LoopingMediaSource extends CompositeMediaSource {
private final MediaSource childSource;
private final int loopCount;
private int childPeriodCount;
+ private Listener listener;
/**
* Loops the provided source indefinitely. Note that it is usually better to use
@@ -61,20 +62,9 @@ public final class LoopingMediaSource implements MediaSource {
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) {
- childSource.prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- childPeriodCount = timeline.getPeriodCount();
- Timeline loopingTimeline = loopCount != Integer.MAX_VALUE
- ? new LoopingTimeline(timeline, loopCount) : new InfinitelyLoopingTimeline(timeline);
- listener.onSourceInfoRefreshed(LoopingMediaSource.this, loopingTimeline, manifest);
- }
- });
- }
-
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- childSource.maybeThrowSourceInfoRefreshError();
+ super.prepareSource(player, isTopLevelSource, listener);
+ this.listener = listener;
+ prepareChildSource(/* id= */ null, childSource);
}
@Override
@@ -92,7 +82,20 @@ public final class LoopingMediaSource implements MediaSource {
@Override
public void releaseSource() {
- childSource.releaseSource();
+ super.releaseSource();
+ listener = null;
+ childPeriodCount = 0;
+ }
+
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Void id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
+ childPeriodCount = timeline.getPeriodCount();
+ Timeline loopingTimeline =
+ loopCount != Integer.MAX_VALUE
+ ? new LoopingTimeline(timeline, loopCount)
+ : new InfinitelyLoopingTimeline(timeline);
+ listener.onSourceInfoRefreshed(this, loopingTimeline, manifest);
}
private static final class LoopingTimeline extends AbstractConcatenatedTimeline {
@@ -103,7 +106,7 @@ public final class LoopingMediaSource implements MediaSource {
private final int loopCount;
public LoopingTimeline(Timeline childTimeline, int loopCount) {
- super(new UnshuffledShuffleOrder(loopCount));
+ super(/* isAtomic= */ false, new UnshuffledShuffleOrder(loopCount));
this.childTimeline = childTimeline;
childPeriodCount = childTimeline.getPeriodCount();
childWindowCount = childTimeline.getWindowCount();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
index c297229d78..a5b2314d78 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException;
@@ -35,27 +36,25 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Called when preparation completes.
- *
- * Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect
- * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be
- * called with the initial track selection.
+ *
+ *
Called on the playback thread. After invoking this method, the {@link MediaPeriod} can
+ * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[],
+ * long)} to be called with the initial track selection.
*
* @param mediaPeriod The prepared {@link MediaPeriod}.
*/
void onPrepared(MediaPeriod mediaPeriod);
-
}
/**
* Prepares this media period asynchronously.
- *
- * {@code callback.onPrepared} is called when preparation completes. If preparation fails,
+ *
+ *
{@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
- *
- * If preparation succeeds and results in a source timeline change (e.g. the period duration
- * becoming known),
- * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be
- * called before {@code callback.onPrepared}.
+ *
+ *
If preparation succeeds and results in a source timeline change (e.g. the period duration
+ * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline,
+ * Object)} will be called before {@code callback.onPrepared}.
*
* @param callback Callback to receive updates from this period, including being notified when
* preparation completes.
@@ -66,8 +65,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Throws an error that's preventing the period from becoming prepared. Does nothing if no such
* error exists.
- *
- * This method should only be called before the period has completed preparation.
+ *
+ *
This method should only be called before the period has completed preparation.
*
* @throws IOException The underlying error.
*/
@@ -75,8 +74,8 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the {@link TrackGroup}s exposed by the period.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
This method should only be called after the period has been prepared.
*
* @return The {@link TrackGroup}s.
*/
@@ -84,16 +83,16 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Performs a track selection.
- *
- * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
+ *
+ *
The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags}
* indicating whether the existing {@code SampleStream} can be retained for each selection, and
* the existing {@code stream}s themselves. The call will update {@code streams} to reflect the
* provided selections, clearing, setting and replacing entries as required. If an existing sample
* stream is retained but with the requirement that the consuming renderer be reset, then the
* corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set
* if a new sample stream is created.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
This method should only be called after the period has been prepared.
*
* @param selections The renderer track selections.
* @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained
@@ -104,28 +103,34 @@ public interface MediaPeriod extends SequenceableLoader {
* @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that
* have been retained but with the requirement that the consuming renderer be reset.
* @param positionUs The current playback position in microseconds. If playback of this period has
- * not yet started, the value will be the starting position.
+ * not yet started, the value will be the starting position.
* @return The actual position at which the tracks were enabled, in microseconds.
*/
- long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
- SampleStream[] streams, boolean[] streamResetFlags, long positionUs);
+ long selectTracks(
+ TrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs);
/**
* Discards buffered media up to the specified position.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
This method should only be called after the period has been prepared.
*
* @param positionUs The position in microseconds.
+ * @param toKeyframe If true then for each track discards samples up to the keyframe before or at
+ * the specified position, rather than any sample before or at that position.
*/
- void discardBuffer(long positionUs);
+ void discardBuffer(long positionUs, boolean toKeyframe);
/**
* Attempts to read a discontinuity.
- *
- * After this method has returned a value other than {@link C#TIME_UNSET}, all
- * {@link SampleStream}s provided by the period are guaranteed to start from a key frame.
- *
- * This method should only be called after the period has been prepared.
+ *
+ *
After this method has returned a value other than {@link C#TIME_UNSET}, all {@link
+ * SampleStream}s provided by the period are guaranteed to start from a key frame.
+ *
+ *
This method should only be called after the period has been prepared.
*
* @return If a discontinuity was read then the playback position in microseconds after the
* discontinuity. Else {@link C#TIME_UNSET}.
@@ -134,23 +139,36 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Attempts to seek to the specified position in microseconds.
- *
- * After this method has been called, all {@link SampleStream}s provided by the period are
+ *
+ *
After this method has been called, all {@link SampleStream}s provided by the period are
* guaranteed to start from a key frame.
- *
- * This method should only be called when at least one track is selected.
+ *
+ *
This method should only be called when at least one track is selected.
*
* @param positionUs The seek position in microseconds.
* @return The actual position to which the period was seeked, in microseconds.
*/
long seekToUs(long positionUs);
+ /**
+ * Returns the position to which a seek will be performed, given the specified seek position and
+ * {@link SeekParameters}.
+ *
+ *
This method should only be called after the period has been prepared.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed. Implementations may
+ * apply seek parameters on a best effort basis.
+ * @return The actual position to which a seek will be performed, in microseconds.
+ */
+ long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
+
// SequenceableLoader interface. Overridden to provide more specific documentation.
/**
* Returns an estimate of the position up to which data is buffered for the enabled tracks.
- *
- * This method should only be called when at least one track is selected.
+ *
+ *
This method should only be called when at least one track is selected.
*
* @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.
@@ -160,19 +178,19 @@ public interface MediaPeriod extends SequenceableLoader {
/**
* Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished.
- *
- * This method should only be called after the period has been prepared. It may be called when no
- * tracks are selected.
+ *
+ *
This method should only be called after the period has been prepared. It may be called when
+ * no tracks are selected.
*/
@Override
long getNextLoadPositionUs();
/**
* Attempts to continue loading.
- *
- * This method may be called both during and after the period has been prepared.
- *
- * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
+ *
+ *
This method may be called both during and after the period has been prepared.
+ *
+ *
A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the
* {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be
* called when the period is permitted to continue loading data. A period may do this both during
* and after preparation.
@@ -180,10 +198,24 @@ public interface MediaPeriod extends SequenceableLoader {
* @param positionUs The current playback position in microseconds. If playback of this period has
* not yet started, the value will be the starting position in this period minus the duration
* of any media in previous periods still to be played.
- * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return
- * a different value than prior to the call. False otherwise.
+ * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a
+ * different value than prior to the call. False otherwise.
*/
@Override
boolean continueLoading(long positionUs);
+ /**
+ * Re-evaluates the buffer given the playback position.
+ *
+ *
This method should only be called after the period has been prepared.
+ *
+ *
A period may choose to discard buffered media so that it can be re-buffered in a different
+ * quality.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position in this period minus the duration
+ * of any media in previous periods still to be played.
+ */
+ @Override
+ void reevaluateBuffer(long positionUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
index 4a0d8e196d..02bd0cdbc7 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java
@@ -63,12 +63,6 @@ public interface MediaSource {
*/
final class MediaPeriodId {
- /**
- * Value for unset media period identifiers.
- */
- public static final MediaPeriodId UNSET =
- new MediaPeriodId(C.INDEX_UNSET, C.INDEX_UNSET, C.INDEX_UNSET);
-
/**
* The timeline period index.
*/
@@ -86,13 +80,32 @@ public interface MediaSource {
*/
public final int adIndexInAdGroup;
+ /**
+ * The sequence number of the window in the buffered sequence of windows this media period is
+ * part of. {@link C#INDEX_UNSET} if the media period id is not part of a buffered sequence of
+ * windows.
+ */
+ public final long windowSequenceNumber;
+
+ /**
+ * Creates a media period identifier for a dummy period which is not part of a buffered sequence
+ * of windows.
+ *
+ * @param periodIndex The period index.
+ */
+ public MediaPeriodId(int periodIndex) {
+ this(periodIndex, C.INDEX_UNSET);
+ }
+
/**
* Creates a media period identifier for the specified period in the timeline.
*
* @param periodIndex The timeline period index.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this media period is part of.
*/
- public MediaPeriodId(int periodIndex) {
- this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET);
+ public MediaPeriodId(int periodIndex, long windowSequenceNumber) {
+ this(periodIndex, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber);
}
/**
@@ -102,19 +115,24 @@ public interface MediaSource {
* @param periodIndex The index of the timeline period that contains the ad group.
* @param adGroupIndex The index of the ad group.
* @param adIndexInAdGroup The index of the ad in the ad group.
+ * @param windowSequenceNumber The sequence number of the window in the buffered sequence of
+ * windows this media period is part of.
*/
- public MediaPeriodId(int periodIndex, int adGroupIndex, int adIndexInAdGroup) {
+ public MediaPeriodId(
+ int periodIndex, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) {
this.periodIndex = periodIndex;
this.adGroupIndex = adGroupIndex;
this.adIndexInAdGroup = adIndexInAdGroup;
+ this.windowSequenceNumber = windowSequenceNumber;
}
/**
* Returns a copy of this period identifier but with {@code newPeriodIndex} as its period index.
*/
public MediaPeriodId copyWithPeriodIndex(int newPeriodIndex) {
- return periodIndex == newPeriodIndex ? this
- : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup);
+ return periodIndex == newPeriodIndex
+ ? this
+ : new MediaPeriodId(newPeriodIndex, adGroupIndex, adIndexInAdGroup, windowSequenceNumber);
}
/**
@@ -134,8 +152,10 @@ public interface MediaSource {
}
MediaPeriodId periodId = (MediaPeriodId) obj;
- return periodIndex == periodId.periodIndex && adGroupIndex == periodId.adGroupIndex
- && adIndexInAdGroup == periodId.adIndexInAdGroup;
+ return periodIndex == periodId.periodIndex
+ && adGroupIndex == periodId.adGroupIndex
+ && adIndexInAdGroup == periodId.adIndexInAdGroup
+ && windowSequenceNumber == periodId.windowSequenceNumber;
}
@Override
@@ -144,11 +164,14 @@ public interface MediaSource {
result = 31 * result + periodIndex;
result = 31 * result + adGroupIndex;
result = 31 * result + adIndexInAdGroup;
+ result = 31 * result + (int) windowSequenceNumber;
return result;
}
}
+ String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused.";
+
/**
* Starts preparation of the source.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
index 4d500f94bd..9fc2572b55 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java
@@ -44,7 +44,7 @@ public interface MediaSourceEventListener {
* @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if
* the load is not for media data.
* @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the
- * load is not for media data.
+ * load is not for media data or the end time is unknown.
* @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began.
*/
void onLoadStarted(
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
index e6a4d4e603..cc0c63ef41 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException;
@@ -30,15 +31,18 @@ import java.util.IdentityHashMap;
public final MediaPeriod[] periods;
private final IdentityHashMap streamPeriodIndices;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private Callback callback;
private int pendingChildPrepareCount;
private TrackGroupArray trackGroups;
private MediaPeriod[] enabledPeriods;
- private SequenceableLoader sequenceableLoader;
+ private SequenceableLoader compositeSequenceableLoader;
- public MergingMediaPeriod(MediaPeriod... periods) {
+ public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ MediaPeriod... periods) {
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
this.periods = periods;
streamPeriodIndices = new IdentityHashMap<>();
}
@@ -124,25 +128,31 @@ import java.util.IdentityHashMap;
// Update the local state.
enabledPeriods = new MediaPeriod[enabledPeriodsList.size()];
enabledPeriodsList.toArray(enabledPeriods);
- sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods);
+ compositeSequenceableLoader =
+ compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods);
return positionUs;
}
@Override
- public void discardBuffer(long positionUs) {
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
for (MediaPeriod period : enabledPeriods) {
- period.discardBuffer(positionUs);
+ period.discardBuffer(positionUs, toKeyframe);
}
}
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ compositeSequenceableLoader.reevaluateBuffer(positionUs);
+ }
+
@Override
public boolean continueLoading(long positionUs) {
- return sequenceableLoader.continueLoading(positionUs);
+ return compositeSequenceableLoader.continueLoading(positionUs);
}
@Override
public long getNextLoadPositionUs() {
- return sequenceableLoader.getNextLoadPositionUs();
+ return compositeSequenceableLoader.getNextLoadPositionUs();
}
@Override
@@ -168,7 +178,7 @@ import java.util.IdentityHashMap;
@Override
public long getBufferedPositionUs() {
- return sequenceableLoader.getBufferedPositionUs();
+ return compositeSequenceableLoader.getBufferedPositionUs();
}
@Override
@@ -183,6 +193,11 @@ import java.util.IdentityHashMap;
return positionUs;
}
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
// MediaPeriod.Callback implementation
@Override
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
index 1550970e47..a738cb1893 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java
@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source;
import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator;
@@ -24,14 +25,14 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
/**
* Merges multiple {@link MediaSource}s.
- *
- * The {@link Timeline}s of the sources being merged must have the same number of periods, and must
- * not have any dynamic windows.
+ *
+ *
The {@link Timeline}s of the sources being merged must have the same number of periods.
*/
-public final class MergingMediaSource implements MediaSource {
+public final class MergingMediaSource extends CompositeMediaSource {
/**
* Thrown when a {@link MergingMediaSource} cannot merge its sources.
@@ -42,26 +43,20 @@ public final class MergingMediaSource implements MediaSource {
* The reason the merge failed.
*/
@Retention(RetentionPolicy.SOURCE)
- @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH})
+ @IntDef({REASON_PERIOD_COUNT_MISMATCH})
public @interface Reason {}
/**
- * The merge failed because one of the sources being merged has a dynamic window.
+ * The sources have different period counts.
*/
- public static final int REASON_WINDOWS_ARE_DYNAMIC = 0;
- /**
- * The merge failed because the sources have different period counts.
- */
- public static final int REASON_PERIOD_COUNT_MISMATCH = 1;
+ public static final int REASON_PERIOD_COUNT_MISMATCH = 0;
/**
- * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
- * {@link #REASON_PERIOD_COUNT_MISMATCH}.
+ * The reason the merge failed.
*/
@Reason public final int reason;
/**
- * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and
- * {@link #REASON_PERIOD_COUNT_MISMATCH}.
+ * @param reason The reason the merge failed.
*/
public IllegalMergeException(@Reason int reason) {
this.reason = reason;
@@ -73,7 +68,7 @@ public final class MergingMediaSource implements MediaSource {
private final MediaSource[] mediaSources;
private final ArrayList pendingTimelineSources;
- private final Timeline.Window window;
+ private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private Listener listener;
private Timeline primaryTimeline;
@@ -85,23 +80,29 @@ public final class MergingMediaSource implements MediaSource {
* @param mediaSources The {@link MediaSource}s to merge.
*/
public MergingMediaSource(MediaSource... mediaSources) {
+ this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources);
+ }
+
+ /**
+ * @param compositeSequenceableLoaderFactory A factory to create composite
+ * {@link SequenceableLoader}s for when this media source loads data from multiple streams
+ * (video, audio etc...).
+ * @param mediaSources The {@link MediaSource}s to merge.
+ */
+ public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
+ MediaSource... mediaSources) {
this.mediaSources = mediaSources;
+ this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources));
- window = new Timeline.Window();
periodCount = PERIOD_COUNT_UNSET;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ super.prepareSource(player, isTopLevelSource, listener);
this.listener = listener;
for (int i = 0; i < mediaSources.length; i++) {
- final int sourceIndex = i;
- mediaSources[sourceIndex].prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- handleSourceInfoRefreshed(sourceIndex, timeline, manifest);
- }
- });
+ prepareChildSource(i, mediaSources[i]);
}
}
@@ -110,9 +111,7 @@ public final class MergingMediaSource implements MediaSource {
if (mergeError != null) {
throw mergeError;
}
- for (MediaSource mediaSource : mediaSources) {
- mediaSource.maybeThrowSourceInfoRefreshError();
- }
+ super.maybeThrowSourceInfoRefreshError();
}
@Override
@@ -121,7 +120,7 @@ public final class MergingMediaSource implements MediaSource {
for (int i = 0; i < periods.length; i++) {
periods[i] = mediaSources[i].createPeriod(id, allocator);
}
- return new MergingMediaPeriod(periods);
+ return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods);
}
@Override
@@ -134,20 +133,27 @@ public final class MergingMediaSource implements MediaSource {
@Override
public void releaseSource() {
- for (MediaSource mediaSource : mediaSources) {
- mediaSource.releaseSource();
- }
+ super.releaseSource();
+ listener = null;
+ primaryTimeline = null;
+ primaryManifest = null;
+ periodCount = PERIOD_COUNT_UNSET;
+ mergeError = null;
+ pendingTimelineSources.clear();
+ Collections.addAll(pendingTimelineSources, mediaSources);
}
- private void handleSourceInfoRefreshed(int sourceIndex, Timeline timeline, Object manifest) {
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ Integer id, MediaSource mediaSource, Timeline timeline, @Nullable Object manifest) {
if (mergeError == null) {
mergeError = checkTimelineMerges(timeline);
}
if (mergeError != null) {
return;
}
- pendingTimelineSources.remove(mediaSources[sourceIndex]);
- if (sourceIndex == 0) {
+ pendingTimelineSources.remove(mediaSource);
+ if (mediaSource == mediaSources[0]) {
primaryTimeline = timeline;
primaryManifest = manifest;
}
@@ -157,12 +163,6 @@ public final class MergingMediaSource implements MediaSource {
}
private IllegalMergeException checkTimelineMerges(Timeline timeline) {
- int windowCount = timeline.getWindowCount();
- for (int i = 0; i < windowCount; i++) {
- if (timeline.getWindow(i, window, false).isDynamic) {
- return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC);
- }
- }
if (periodCount == PERIOD_COUNT_UNSET) {
periodCount = timeline.getPeriodCount();
} else if (timeline.getPeriodCount() != periodCount) {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
index d70c59b195..e5b950cf2e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java
@@ -51,8 +51,8 @@ import com.google.android.exoplayer2.util.Util;
private Format[] formats;
private int length;
- private int absoluteStartIndex;
- private int relativeStartIndex;
+ private int absoluteFirstIndex;
+ private int relativeFirstIndex;
private int readPosition;
private long largestDiscardedTimestampUs;
@@ -87,8 +87,8 @@ import com.google.android.exoplayer2.util.Util;
*/
public void reset(boolean resetUpstreamFormat) {
length = 0;
- absoluteStartIndex = 0;
- relativeStartIndex = 0;
+ absoluteFirstIndex = 0;
+ relativeFirstIndex = 0;
readPosition = 0;
upstreamKeyframeRequired = true;
largestDiscardedTimestampUs = Long.MIN_VALUE;
@@ -103,7 +103,7 @@ import com.google.android.exoplayer2.util.Util;
* Returns the current absolute write index.
*/
public int getWriteIndex() {
- return absoluteStartIndex + length;
+ return absoluteFirstIndex + length;
}
/**
@@ -132,11 +132,18 @@ import com.google.android.exoplayer2.util.Util;
// Called by the consuming thread.
+ /**
+ * Returns the current absolute start index.
+ */
+ public int getFirstIndex() {
+ return absoluteFirstIndex;
+ }
+
/**
* Returns the current absolute read index.
*/
public int getReadIndex() {
- return absoluteStartIndex + readPosition;
+ return absoluteFirstIndex + readPosition;
}
/**
@@ -179,6 +186,11 @@ import com.google.android.exoplayer2.util.Util;
return largestQueuedTimestampUs;
}
+ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
+ public synchronized long getFirstTimestampUs() {
+ return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex];
+ }
+
/**
* Rewinds the read position to the first sample retained in the queue.
*/
@@ -284,6 +296,22 @@ import com.google.android.exoplayer2.util.Util;
return skipCount;
}
+ /**
+ * Attempts to set the read position to the specified sample index.
+ *
+ * @param sampleIndex The sample index.
+ * @return Whether the read position was set successfully. False is returned if the specified
+ * index is smaller than the index of the first sample in the queue, or larger than the index
+ * of the next sample that will be written.
+ */
+ public synchronized boolean setReadPosition(int sampleIndex) {
+ if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) {
+ readPosition = sampleIndex - absoluteFirstIndex;
+ return true;
+ }
+ return false;
+ }
+
/**
* Discards up to but not including the sample immediately before or at the specified time.
*
@@ -297,11 +325,11 @@ import com.google.android.exoplayer2.util.Util;
* {@link C#POSITION_UNSET} if no discarding of data is necessary.
*/
public synchronized long discardTo(long timeUs, boolean toKeyframe, boolean stopAtReadPosition) {
- if (length == 0 || timeUs < timesUs[relativeStartIndex]) {
+ if (length == 0 || timeUs < timesUs[relativeFirstIndex]) {
return C.POSITION_UNSET;
}
int searchLength = stopAtReadPosition && readPosition != length ? readPosition + 1 : length;
- int discardCount = findSampleBefore(relativeStartIndex, searchLength, timeUs, toKeyframe);
+ int discardCount = findSampleBefore(relativeFirstIndex, searchLength, timeUs, toKeyframe);
if (discardCount == -1) {
return C.POSITION_UNSET;
}
@@ -382,15 +410,15 @@ import com.google.android.exoplayer2.util.Util;
int[] newSizes = new int[newCapacity];
CryptoData[] newCryptoDatas = new CryptoData[newCapacity];
Format[] newFormats = new Format[newCapacity];
- int beforeWrap = capacity - relativeStartIndex;
- System.arraycopy(offsets, relativeStartIndex, newOffsets, 0, beforeWrap);
- System.arraycopy(timesUs, relativeStartIndex, newTimesUs, 0, beforeWrap);
- System.arraycopy(flags, relativeStartIndex, newFlags, 0, beforeWrap);
- System.arraycopy(sizes, relativeStartIndex, newSizes, 0, beforeWrap);
- System.arraycopy(cryptoDatas, relativeStartIndex, newCryptoDatas, 0, beforeWrap);
- System.arraycopy(formats, relativeStartIndex, newFormats, 0, beforeWrap);
- System.arraycopy(sourceIds, relativeStartIndex, newSourceIds, 0, beforeWrap);
- int afterWrap = relativeStartIndex;
+ int beforeWrap = capacity - relativeFirstIndex;
+ System.arraycopy(offsets, relativeFirstIndex, newOffsets, 0, beforeWrap);
+ System.arraycopy(timesUs, relativeFirstIndex, newTimesUs, 0, beforeWrap);
+ System.arraycopy(flags, relativeFirstIndex, newFlags, 0, beforeWrap);
+ System.arraycopy(sizes, relativeFirstIndex, newSizes, 0, beforeWrap);
+ System.arraycopy(cryptoDatas, relativeFirstIndex, newCryptoDatas, 0, beforeWrap);
+ System.arraycopy(formats, relativeFirstIndex, newFormats, 0, beforeWrap);
+ System.arraycopy(sourceIds, relativeFirstIndex, newSourceIds, 0, beforeWrap);
+ int afterWrap = relativeFirstIndex;
System.arraycopy(offsets, 0, newOffsets, beforeWrap, afterWrap);
System.arraycopy(timesUs, 0, newTimesUs, beforeWrap, afterWrap);
System.arraycopy(flags, 0, newFlags, beforeWrap, afterWrap);
@@ -405,7 +433,7 @@ import com.google.android.exoplayer2.util.Util;
cryptoDatas = newCryptoDatas;
formats = newFormats;
sourceIds = newSourceIds;
- relativeStartIndex = 0;
+ relativeFirstIndex = 0;
length = capacity;
capacity = newCapacity;
}
@@ -440,7 +468,7 @@ import com.google.android.exoplayer2.util.Util;
relativeSampleIndex = capacity - 1;
}
}
- discardUpstreamSamples(absoluteStartIndex + retainCount);
+ discardUpstreamSamples(absoluteFirstIndex + retainCount);
return true;
}
@@ -454,7 +482,7 @@ import com.google.android.exoplayer2.util.Util;
* @param length The length of the range being searched.
* @param timeUs The specified time.
* @param keyframe Whether only keyframes should be considered.
- * @return The offset from {@code relativeStartIndex} to the found sample, or -1 if no matching
+ * @return The offset from {@code relativeFirstIndex} to the found sample, or -1 if no matching
* sample was found.
*/
private int findSampleBefore(int relativeStartIndex, int length, long timeUs, boolean keyframe) {
@@ -480,27 +508,26 @@ import com.google.android.exoplayer2.util.Util;
* Discards the specified number of samples.
*
* @param discardCount The number of samples to discard.
- * @return The corresponding offset up to which data should be discarded, or
- * {@link C#POSITION_UNSET} if no discarding of data is necessary.
+ * @return The corresponding offset up to which data should be discarded.
*/
private long discardSamples(int discardCount) {
largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs,
getLargestTimestamp(discardCount));
length -= discardCount;
- absoluteStartIndex += discardCount;
- relativeStartIndex += discardCount;
- if (relativeStartIndex >= capacity) {
- relativeStartIndex -= capacity;
+ absoluteFirstIndex += discardCount;
+ relativeFirstIndex += discardCount;
+ if (relativeFirstIndex >= capacity) {
+ relativeFirstIndex -= capacity;
}
readPosition -= discardCount;
if (readPosition < 0) {
readPosition = 0;
}
if (length == 0) {
- int relativeLastDiscardIndex = (relativeStartIndex == 0 ? capacity : relativeStartIndex) - 1;
+ int relativeLastDiscardIndex = (relativeFirstIndex == 0 ? capacity : relativeFirstIndex) - 1;
return offsets[relativeLastDiscardIndex] + sizes[relativeLastDiscardIndex];
} else {
- return offsets[relativeStartIndex];
+ return offsets[relativeFirstIndex];
}
}
@@ -537,7 +564,7 @@ import com.google.android.exoplayer2.util.Util;
* @param offset The offset, which must be in the range [0, length].
*/
private int getRelativeIndex(int offset) {
- int relativeIndex = relativeStartIndex + offset;
+ int relativeIndex = relativeFirstIndex + offset;
return relativeIndex < capacity ? relativeIndex : relativeIndex - capacity;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
index b83cf7df5b..d9090baf3b 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java
@@ -181,6 +181,13 @@ public final class SampleQueue implements TrackOutput {
return metadataQueue.hasNextSample();
}
+ /**
+ * Returns the absolute index of the first sample.
+ */
+ public int getFirstIndex() {
+ return metadataQueue.getFirstIndex();
+ }
+
/**
* Returns the current absolute read index.
*/
@@ -219,6 +226,11 @@ public final class SampleQueue implements TrackOutput {
return metadataQueue.getLargestQueuedTimestampUs();
}
+ /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */
+ public long getFirstTimestampUs() {
+ return metadataQueue.getFirstTimestampUs();
+ }
+
/**
* Rewinds the read position to the first sample in the queue.
*/
@@ -281,6 +293,18 @@ public final class SampleQueue implements TrackOutput {
return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer);
}
+ /**
+ * Attempts to set the read position to the specified sample index.
+ *
+ * @param sampleIndex The sample index.
+ * @return Whether the read position was set successfully. False is returned if the specified
+ * index is smaller than the index of the first sample in the queue, or larger than the index
+ * of the next sample that will be written.
+ */
+ public boolean setReadPosition(int sampleIndex) {
+ return metadataQueue.setReadPosition(sampleIndex);
+ }
+
/**
* Attempts to read from the queue.
*
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
index 6daa1e847a..182f0f17cc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java
@@ -60,4 +60,15 @@ public interface SequenceableLoader {
*/
boolean continueLoading(long positionUs);
+ /**
+ * Re-evaluates the buffer given the playback position.
+ *
+ * Re-evaluation may discard buffered media so that it can be re-buffered in a different
+ * quality.
+ *
+ * @param positionUs The current playback position in microseconds. If playback of this period has
+ * not yet started, the value will be the starting position in this period minus the duration
+ * of any media in previous periods still to be played.
+ */
+ void reevaluateBuffer(long positionUs);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java
index 4307fd2c19..f5f98e4d8a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java
@@ -136,6 +136,11 @@ public interface ShuffleOrder {
return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong()));
}
+ @Override
+ public ShuffleOrder cloneAndClear() {
+ return new DefaultShuffleOrder(/* length= */ 0, new Random(random.nextLong()));
+ }
+
private static int[] createShuffledList(int length, Random random) {
int[] shuffled = new int[length];
for (int i = 0; i < length; i++) {
@@ -199,6 +204,10 @@ public interface ShuffleOrder {
return new UnshuffledShuffleOrder(length - 1);
}
+ @Override
+ public ShuffleOrder cloneAndClear() {
+ return new UnshuffledShuffleOrder(/* length= */ 0);
+ }
}
/**
@@ -237,7 +246,7 @@ public interface ShuffleOrder {
int getFirstIndex();
/**
- * Return a copy of the shuffle order with newly inserted elements.
+ * Returns 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}.
@@ -246,11 +255,13 @@ public interface ShuffleOrder {
ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount);
/**
- * Return a copy of the shuffle order with one element removed.
+ * Returns 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);
+ /** Returns a copy of the shuffle order with all elements removed. */
+ ShuffleOrder cloneAndClear();
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
index 6f35438444..9cce67f68c 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java
@@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline {
private final boolean isDynamic;
/**
- * Creates a timeline of one period of known duration, and a static window starting at zero and
- * extending to that duration.
+ * Creates a timeline containing a single period and a window that spans it.
*
* @param durationUs The duration of the period, in microseconds.
* @param isSeekable Whether seeking is supported within the period.
+ * @param isDynamic Whether the window may change when the timeline is updated.
*/
- public SinglePeriodTimeline(long durationUs, boolean isSeekable) {
- this(durationUs, durationUs, 0, 0, isSeekable, false);
+ public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) {
+ this(durationUs, durationUs, 0, 0, isSeekable, isDynamic);
}
/**
@@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline {
long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable,
boolean isDynamic) {
this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs,
- windowDefaultStartPositionUs, isSeekable, isDynamic);
+ windowDefaultStartPositionUs, isSeekable, isDynamic);
}
/**
@@ -106,11 +106,16 @@ public final class SinglePeriodTimeline extends Timeline {
Assertions.checkIndex(windowIndex, 0, 1);
Object id = setIds ? ID : null;
long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
- if (isDynamic) {
- windowDefaultStartPositionUs += defaultPositionProjectionUs;
- if (windowDefaultStartPositionUs > windowDurationUs) {
- // The projection takes us beyond the end of the live window.
+ if (isDynamic && defaultPositionProjectionUs != 0) {
+ if (windowDurationUs == C.TIME_UNSET) {
+ // Don't allow projection into a window that has an unknown duration.
windowDefaultStartPositionUs = C.TIME_UNSET;
+ } else {
+ windowDefaultStartPositionUs += defaultPositionProjectionUs;
+ if (windowDefaultStartPositionUs > windowDurationUs) {
+ // The projection takes us beyond the end of the window.
+ windowDefaultStartPositionUs = C.TIME_UNSET;
+ }
}
}
return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic,
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
index 5069a2d633..36e5d910c4 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java
@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@@ -25,6 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
+import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.util.ArrayList;
@@ -118,7 +120,12 @@ import java.util.Arrays;
}
@Override
- public void discardBuffer(long positionUs) {
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ // Do nothing.
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
@@ -127,10 +134,21 @@ import java.util.Arrays;
if (loadingFinished || loader.isLoading()) {
return false;
}
- loader.startLoading(
- new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()),
- this,
- minLoadableRetryCount);
+ long elapsedRealtimeMs =
+ loader.startLoading(
+ new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()),
+ this,
+ minLoadableRetryCount);
+ eventDispatcher.loadStarted(
+ dataSpec,
+ C.DATA_TYPE_MEDIA,
+ C.TRACK_TYPE_UNKNOWN,
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaStartTimeUs= */ 0,
+ durationUs,
+ elapsedRealtimeMs);
return true;
}
@@ -152,11 +170,16 @@ import java.util.Arrays;
@Override
public long seekToUs(long positionUs) {
for (int i = 0; i < sampleStreams.size(); i++) {
- sampleStreams.get(i).seekToUs(positionUs);
+ sampleStreams.get(i).reset();
}
return positionUs;
}
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return positionUs;
+ }
+
// Loader.Callback implementation.
@Override
@@ -230,8 +253,9 @@ import java.util.Arrays;
private static final int STREAM_STATE_END_OF_STREAM = 2;
private int streamState;
+ private boolean formatSent;
- public void seekToUs(long positionUs) {
+ public void reset() {
if (streamState == STREAM_STATE_END_OF_STREAM) {
streamState = STREAM_STATE_SEND_SAMPLE;
}
@@ -265,6 +289,7 @@ import java.util.Arrays;
buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
buffer.ensureSpaceForWrite(sampleSize);
buffer.data.put(sampleData, 0, sampleSize);
+ sendFormat();
} else {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
}
@@ -278,11 +303,23 @@ import java.util.Arrays;
public int skipData(long positionUs) {
if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) {
streamState = STREAM_STATE_END_OF_STREAM;
+ sendFormat();
return 1;
}
return 0;
}
+ private void sendFormat() {
+ if (!formatSent) {
+ eventDispatcher.downstreamFormatChanged(
+ MimeTypes.getTrackType(format.sampleMimeType),
+ format,
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ /* mediaTimeUs= */ 0);
+ formatSent = true;
+ }
+ }
}
/* package */ static final class SourceLoadable implements Loadable {
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
index 3b0a5a16c8..b92085d15e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java
@@ -244,7 +244,7 @@ public final class SingleSampleMediaSource implements MediaSource {
this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream;
this.eventDispatcher = new EventDispatcher(eventHandler, eventListener);
dataSpec = new DataSpec(uri);
- timeline = new SinglePeriodTimeline(durationUs, true);
+ timeline = new SinglePeriodTimeline(durationUs, true, false);
}
// MediaSource implementation.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
index 58fa149b59..8654e94bdb 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java
@@ -16,49 +16,237 @@
package com.google.android.exoplayer2.source.ads;
import android.net.Uri;
+import android.support.annotation.CheckResult;
+import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.util.Assertions;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
/**
- * Represents the structure of ads to play and the state of loaded/played ads.
+ * Represents ad group times relative to the start of the media and information on the state and
+ * URIs of ads within each ad group.
+ *
+ *
Instances are immutable. Call the {@code with*} methods to get new instances that have the
+ * required changes.
*/
public final class AdPlaybackState {
/**
- * The number of ad groups.
+ * Represents a group of ads, with information about their states.
+ *
+ *
Instances are immutable. Call the {@code with*} methods to get new instances that have the
+ * required changes.
*/
+ public static final class AdGroup {
+
+ /** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
+ public final int count;
+ /** The URI of each ad in the ad group. */
+ public final Uri[] uris;
+ /** The state of each ad in the ad group. */
+ public final @AdState int[] states;
+ /** The durations of each ad in the ad group, in microseconds. */
+ public final long[] durationsUs;
+
+ /** Creates a new ad group with an unspecified number of ads. */
+ public AdGroup() {
+ this(
+ /* count= */ C.LENGTH_UNSET,
+ /* states= */ new int[0],
+ /* uris= */ new Uri[0],
+ /* durationsUs= */ new long[0]);
+ }
+
+ private AdGroup(int count, @AdState int[] states, Uri[] uris, long[] durationsUs) {
+ Assertions.checkArgument(states.length == uris.length);
+ this.count = count;
+ this.states = states;
+ this.uris = uris;
+ this.durationsUs = durationsUs;
+ }
+
+ /**
+ * Returns the index of the first ad in the ad group that should be played, or {@link #count} if
+ * no ads should be played.
+ */
+ public int getFirstAdIndexToPlay() {
+ return getNextAdIndexToPlay(-1);
+ }
+
+ /**
+ * Returns the index of the next ad in the ad group that should be played after playing {@code
+ * lastPlayedAdIndex}, or {@link #count} if no later ads should be played.
+ */
+ public int getNextAdIndexToPlay(int lastPlayedAdIndex) {
+ int nextAdIndexToPlay = lastPlayedAdIndex + 1;
+ while (nextAdIndexToPlay < states.length) {
+ if (states[nextAdIndexToPlay] == AD_STATE_UNAVAILABLE
+ || states[nextAdIndexToPlay] == AD_STATE_AVAILABLE) {
+ break;
+ }
+ nextAdIndexToPlay++;
+ }
+ return nextAdIndexToPlay;
+ }
+
+ /** Returns whether the ad group has at least one ad that still needs to be played. */
+ public boolean hasUnplayedAds() {
+ return count == C.LENGTH_UNSET || getFirstAdIndexToPlay() < count;
+ }
+
+ /**
+ * Returns a new instance with the ad count set to {@code count}. This method may only be called
+ * if this instance's ad count has not yet been specified.
+ */
+ @CheckResult
+ public AdGroup withAdCount(int count) {
+ Assertions.checkArgument(this.count == C.LENGTH_UNSET && states.length <= count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, count);
+ long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
+ Uri[] uris = Arrays.copyOf(this.uris, count);
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns a new instance with the specified {@code uri} set for the specified ad, and the ad
+ * marked as {@link #AD_STATE_AVAILABLE}. The specified ad must currently be in {@link
+ * #AD_STATE_UNAVAILABLE}, which is the default state.
+ *
+ *
This instance's ad count may be unknown, in which case {@code index} must be less than the
+ * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
+ */
+ @CheckResult
+ public AdGroup withAdUri(Uri uri, int index) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
+ Assertions.checkArgument(states[index] == AD_STATE_UNAVAILABLE);
+ long[] durationsUs =
+ this.durationsUs.length == states.length
+ ? this.durationsUs
+ : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
+ Uri[] uris = Arrays.copyOf(this.uris, states.length);
+ uris[index] = uri;
+ states[index] = AD_STATE_AVAILABLE;
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns a new instance with the specified ad set to the specified {@code state}. The ad
+ * specified must currently either be in {@link #AD_STATE_UNAVAILABLE} or {@link
+ * #AD_STATE_AVAILABLE}.
+ *
+ *
This instance's ad count may be unknown, in which case {@code index} must be less than the
+ * ad count specified later. Otherwise, {@code index} must be less than the current ad count.
+ */
+ @CheckResult
+ public AdGroup withAdState(@AdState int state, int index) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || index < count);
+ @AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
+ Assertions.checkArgument(
+ states[index] == AD_STATE_UNAVAILABLE
+ || states[index] == AD_STATE_AVAILABLE
+ || states[index] == state);
+ long[] durationsUs =
+ this.durationsUs.length == states.length
+ ? this.durationsUs
+ : copyDurationsUsWithSpaceForAdCount(this.durationsUs, states.length);
+ Uri[] uris =
+ this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
+ states[index] = state;
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /** Returns a new instance with the specified ad durations, in microseconds. */
+ @CheckResult
+ public AdGroup withAdDurationsUs(long[] durationsUs) {
+ Assertions.checkArgument(count == C.LENGTH_UNSET || durationsUs.length <= this.uris.length);
+ if (durationsUs.length < this.uris.length) {
+ durationsUs = copyDurationsUsWithSpaceForAdCount(durationsUs, uris.length);
+ }
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ /**
+ * Returns an instance with all unavailable and available ads marked as skipped. If the ad count
+ * hasn't been set, it will be set to zero.
+ */
+ @CheckResult
+ public AdGroup withAllAdsSkipped() {
+ if (count == C.LENGTH_UNSET) {
+ return new AdGroup(
+ /* count= */ 0,
+ /* states= */ new int[0],
+ /* uris= */ new Uri[0],
+ /* durationsUs= */ new long[0]);
+ }
+ int count = this.states.length;
+ @AdState int[] states = Arrays.copyOf(this.states, count);
+ for (int i = 0; i < count; i++) {
+ if (states[i] == AD_STATE_AVAILABLE || states[i] == AD_STATE_UNAVAILABLE) {
+ states[i] = AD_STATE_SKIPPED;
+ }
+ }
+ return new AdGroup(count, states, uris, durationsUs);
+ }
+
+ @CheckResult
+ private static @AdState int[] copyStatesWithSpaceForAdCount(@AdState int[] states, int count) {
+ int oldStateCount = states.length;
+ int newStateCount = Math.max(count, oldStateCount);
+ states = Arrays.copyOf(states, newStateCount);
+ Arrays.fill(states, oldStateCount, newStateCount, AD_STATE_UNAVAILABLE);
+ return states;
+ }
+
+ @CheckResult
+ private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int count) {
+ int oldDurationsUsCount = durationsUs.length;
+ int newDurationsUsCount = Math.max(count, oldDurationsUsCount);
+ durationsUs = Arrays.copyOf(durationsUs, newDurationsUsCount);
+ Arrays.fill(durationsUs, oldDurationsUsCount, newDurationsUsCount, C.TIME_UNSET);
+ return durationsUs;
+ }
+ }
+
+ /** Represents the state of an ad in an ad group. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ AD_STATE_UNAVAILABLE,
+ AD_STATE_AVAILABLE,
+ AD_STATE_SKIPPED,
+ AD_STATE_PLAYED,
+ AD_STATE_ERROR,
+ })
+ public @interface AdState {}
+ /** State for an ad that does not yet have a URL. */
+ public static final int AD_STATE_UNAVAILABLE = 0;
+ /** State for an ad that has a URL but has not yet been played. */
+ public static final int AD_STATE_AVAILABLE = 1;
+ /** State for an ad that was skipped. */
+ public static final int AD_STATE_SKIPPED = 2;
+ /** State for an ad that was played in full. */
+ public static final int AD_STATE_PLAYED = 3;
+ /** State for an ad that could not be loaded. */
+ public static final int AD_STATE_ERROR = 4;
+
+ /** Ad playback state with no ads. */
+ public static final AdPlaybackState NONE = new AdPlaybackState();
+
+ /** The number of ad groups. */
public final int adGroupCount;
/**
- * The times of ad groups, in microseconds. A final element with the value
- * {@link C#TIME_END_OF_SOURCE} indicates a postroll ad.
+ * The times of ad groups, in microseconds. A final element with the value {@link
+ * C#TIME_END_OF_SOURCE} indicates a postroll ad.
*/
public final long[] adGroupTimesUs;
- /**
- * The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET} if the number of
- * ads is not yet known.
- */
- public final int[] adCounts;
- /**
- * The number of ads loaded so far in each ad group.
- */
- public final int[] adsLoadedCounts;
- /**
- * The number of ads played so far in each ad group.
- */
- public final int[] adsPlayedCounts;
- /**
- * The URI of each ad in each ad group.
- */
- public final Uri[][] adUris;
-
- /**
- * The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise.
- */
- public long contentDurationUs;
- /**
- * The position offset in the first unplayed ad at which to begin playback, in microseconds.
- */
- public long adResumePositionUs;
+ /** The ad groups. */
+ public final AdGroup[] adGroups;
+ /** The position offset in the first unplayed ad at which to begin playback, in microseconds. */
+ public final long adResumePositionUs;
+ /** The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. */
+ public final long contentDurationUs;
/**
* Creates a new ad playback state with the specified ad group times.
@@ -66,85 +254,143 @@ public final class AdPlaybackState {
* @param adGroupTimesUs The times of ad groups in microseconds. A final element with the value
* {@link C#TIME_END_OF_SOURCE} indicates that there is a postroll ad.
*/
- public AdPlaybackState(long[] adGroupTimesUs) {
- this.adGroupTimesUs = adGroupTimesUs;
- adGroupCount = adGroupTimesUs.length;
- adsPlayedCounts = new int[adGroupCount];
- adCounts = new int[adGroupCount];
- Arrays.fill(adCounts, C.LENGTH_UNSET);
- adUris = new Uri[adGroupCount][];
- Arrays.fill(adUris, new Uri[0]);
- adsLoadedCounts = new int[adGroupTimesUs.length];
+ public AdPlaybackState(long... adGroupTimesUs) {
+ int count = adGroupTimesUs.length;
+ adGroupCount = count;
+ this.adGroupTimesUs = Arrays.copyOf(adGroupTimesUs, count);
+ this.adGroups = new AdGroup[count];
+ for (int i = 0; i < count; i++) {
+ adGroups[i] = new AdGroup();
+ }
+ adResumePositionUs = 0;
contentDurationUs = C.TIME_UNSET;
}
- private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts,
- int[] adsPlayedCounts, Uri[][] adUris, long contentDurationUs, long adResumePositionUs) {
+ private AdPlaybackState(
+ long[] adGroupTimesUs, AdGroup[] adGroups, long adResumePositionUs, long contentDurationUs) {
+ adGroupCount = adGroups.length;
this.adGroupTimesUs = adGroupTimesUs;
- this.adCounts = adCounts;
- this.adsLoadedCounts = adsLoadedCounts;
- this.adsPlayedCounts = adsPlayedCounts;
- this.adUris = adUris;
+ this.adGroups = adGroups;
+ this.adResumePositionUs = adResumePositionUs;
this.contentDurationUs = contentDurationUs;
- this.adResumePositionUs = adResumePositionUs;
- adGroupCount = adGroupTimesUs.length;
}
/**
- * Returns a deep copy of this instance.
+ * Returns the index of the ad group at or before {@code positionUs}, if that ad group is
+ * unplayed. Returns {@link C#INDEX_UNSET} if the ad group at or before {@code positionUs} has no
+ * ads remaining to be played, or if there is no such ad group.
+ *
+ * @param positionUs The position at or before which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/
- public AdPlaybackState copy() {
- Uri[][] adUris = new Uri[adGroupTimesUs.length][];
- for (int i = 0; i < this.adUris.length; i++) {
- adUris[i] = Arrays.copyOf(this.adUris[i], this.adUris[i].length);
+ public int getAdGroupIndexForPositionUs(long positionUs) {
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = adGroupTimesUs.length - 1;
+ while (index >= 0
+ && (adGroupTimesUs[index] == C.TIME_END_OF_SOURCE || adGroupTimesUs[index] > positionUs)) {
+ index--;
}
- return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount),
- Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount),
- Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs,
- adResumePositionUs);
+ return index >= 0 && adGroups[index].hasUnplayedAds() ? index : C.INDEX_UNSET;
}
/**
- * Sets the number of ads in the specified ad group.
+ * Returns the index of the next ad group after {@code positionUs} that has ads remaining to be
+ * played. Returns {@link C#INDEX_UNSET} if there is no such ad group.
+ *
+ * @param positionUs The position after which to find an ad group, in microseconds.
+ * @return The index of the ad group, or {@link C#INDEX_UNSET}.
*/
- public void setAdCount(int adGroupIndex, int adCount) {
- adCounts[adGroupIndex] = adCount;
- }
-
- /**
- * Adds an ad to the specified ad group.
- */
- public void addAdUri(int adGroupIndex, Uri uri) {
- int adIndexInAdGroup = adUris[adGroupIndex].length;
- adUris[adGroupIndex] = Arrays.copyOf(adUris[adGroupIndex], adIndexInAdGroup + 1);
- adUris[adGroupIndex][adIndexInAdGroup] = uri;
- adsLoadedCounts[adGroupIndex]++;
- }
-
- /**
- * Marks the last ad in the specified ad group as played.
- */
- public void playedAd(int adGroupIndex) {
- adResumePositionUs = 0;
- adsPlayedCounts[adGroupIndex]++;
- }
-
- /**
- * Marks all ads in the specified ad group as played.
- */
- public void playedAdGroup(int adGroupIndex) {
- adResumePositionUs = 0;
- if (adCounts[adGroupIndex] == C.LENGTH_UNSET) {
- adCounts[adGroupIndex] = 0;
+ public int getAdGroupIndexAfterPositionUs(long positionUs) {
+ // Use a linear search as the array elements may not be increasing due to TIME_END_OF_SOURCE.
+ // In practice we expect there to be few ad groups so the search shouldn't be expensive.
+ int index = 0;
+ while (index < adGroupTimesUs.length
+ && adGroupTimesUs[index] != C.TIME_END_OF_SOURCE
+ && (positionUs >= adGroupTimesUs[index] || !adGroups[index].hasUnplayedAds())) {
+ index++;
}
- adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex];
+ return index < adGroupTimesUs.length ? index : C.INDEX_UNSET;
}
/**
- * Sets the position offset in the first unplayed ad at which to begin playback, in microseconds.
+ * Returns an instance with the number of ads in {@code adGroupIndex} resolved to {@code adCount}.
+ * The ad count must be greater than zero.
*/
- public void setAdResumePositionUs(long adResumePositionUs) {
- this.adResumePositionUs = adResumePositionUs;
+ @CheckResult
+ public AdPlaybackState withAdCount(int adGroupIndex, int adCount) {
+ Assertions.checkArgument(adCount > 0);
+ if (adGroups[adGroupIndex].count == adCount) {
+ return this;
+ }
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = this.adGroups[adGroupIndex].withAdCount(adCount);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad URI. */
+ @CheckResult
+ public AdPlaybackState withAdUri(int adGroupIndex, int adIndexInAdGroup, Uri uri) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdUri(uri, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad marked as played. */
+ @CheckResult
+ public AdPlaybackState withPlayedAd(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_PLAYED, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad marked as having a load error. */
+ @CheckResult
+ public AdPlaybackState withAdLoadError(int adGroupIndex, int adIndexInAdGroup) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdState(AD_STATE_ERROR, adIndexInAdGroup);
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /**
+ * Returns an instance with all ads in the specified ad group skipped (except for those already
+ * marked as played or in the error state).
+ */
+ @CheckResult
+ public AdPlaybackState withSkippedAdGroup(int adGroupIndex) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAllAdsSkipped();
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad durations, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdDurationsUs(long[][] adDurationUs) {
+ AdGroup[] adGroups = Arrays.copyOf(this.adGroups, this.adGroups.length);
+ for (int adGroupIndex = 0; adGroupIndex < adGroupCount; adGroupIndex++) {
+ adGroups[adGroupIndex] = adGroups[adGroupIndex].withAdDurationsUs(adDurationUs[adGroupIndex]);
+ }
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+
+ /** Returns an instance with the specified ad resume position, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withAdResumePositionUs(long adResumePositionUs) {
+ if (this.adResumePositionUs == adResumePositionUs) {
+ return this;
+ } else {
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
+ }
+
+ /** Returns an instance with the specified content duration, in microseconds. */
+ @CheckResult
+ public AdPlaybackState withContentDurationUs(long contentDurationUs) {
+ if (this.contentDurationUs == contentDurationUs) {
+ return this;
+ } else {
+ return new AdPlaybackState(adGroupTimesUs, adGroups, adResumePositionUs, contentDurationUs);
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
index 99feccd2f3..6295ca4229 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java
@@ -22,22 +22,22 @@ import java.io.IOException;
/**
* Interface for loaders of ads, which can be used with {@link AdsMediaSource}.
- *
- * Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In
+ *
+ *
Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In
* particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)}
* with a new copy of the current {@link AdPlaybackState} whenever further information about ads
* becomes known (for example, when an ad media URI is available, or an ad has played to the end).
- *
- * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} will be called when the ads media
+ *
+ *
{@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} will be called when the ads media
* source first initializes, at which point the loader can request ads. If the player enters the
* background, {@link #detachPlayer()} will be called. Loaders should maintain any ad playback state
* in preparation for a later call to {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. If
- * an ad is playing when the player is detached, store the current playback position via
- * {@link AdPlaybackState#setAdResumePositionUs(long)}.
- *
- * If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the implementation
- * of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the same listener to
- * provide the existing playback state to the new player.
+ * an ad is playing when the player is detached, update the ad playback state with the current
+ * playback position using {@link AdPlaybackState#withAdResumePositionUs(long)}.
+ *
+ *
If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the
+ * implementation of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the
+ * same listener to provide the existing playback state to the new player.
*/
public interface AdsLoader {
@@ -54,11 +54,19 @@ public interface AdsLoader {
void onAdPlaybackState(AdPlaybackState adPlaybackState);
/**
- * Called when there was an error loading ads.
+ * Called when there was an error loading ads. The loader will skip the problematic ad(s).
*
* @param error The error.
*/
- void onLoadError(IOException error);
+ void onAdLoadError(IOException error);
+
+ /**
+ * Called when an unexpected internal error is encountered while loading ads. The loader will
+ * skip all remaining ads, as the error is not recoverable.
+ *
+ * @param error The error.
+ */
+ void onInternalAdLoadError(RuntimeException error);
/**
* Called when the user clicks through an ad (for example, following a 'learn more' link).
@@ -103,4 +111,13 @@ public interface AdsLoader {
*/
void release();
+ /**
+ * Notifies the ads loader that the player was not able to prepare media for a given ad.
+ * Implementations should update the ad playback state as the specified ad has failed to load.
+ *
+ * @param adGroupIndex The index of the ad group.
+ * @param adIndexInAdGroup The index of the ad in the ad group.
+ * @param exception The preparation error.
+ */
+ void handlePrepareError(int adGroupIndex, int adIndexInAdGroup, IOException exception);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
index 0980e9d011..64bab7ed96 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java
@@ -24,10 +24,12 @@ import android.view.ViewGroup;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.source.CompositeMediaSource;
import com.google.android.exoplayer2.source.DeferredMediaPeriod;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -39,10 +41,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-/**
- * A {@link MediaSource} that inserts ads linearly with a provided content media source.
- */
-public final class AdsMediaSource implements MediaSource {
+/** A {@link MediaSource} that inserts ads linearly with a provided content media source. */
+public final class AdsMediaSource extends CompositeMediaSource {
/** Factory for creating {@link MediaSource}s to play ad media. */
public interface MediaSourceFactory {
@@ -73,14 +73,21 @@ public final class AdsMediaSource implements MediaSource {
public interface EventListener extends MediaSourceEventListener {
/**
- * Called if there was an error loading ads. The media source will load the content without ads
- * if ads can't be loaded, so listen for this event if you need to implement additional handling
- * (for example, stopping the player).
+ * Called if there was an error loading one or more ads. The loader will skip the problematic
+ * ad(s).
*
* @param error The error.
*/
void onAdLoadError(IOException error);
+ /**
+ * Called when an unexpected internal error is encountered while loading ads. The loader will
+ * skip all remaining ads, as the error is not recoverable.
+ *
+ * @param error The error.
+ */
+ void onInternalAdLoadError(RuntimeException error);
+
/**
* Called when the user clicks through an ad (for example, following a 'learn more' link).
*/
@@ -102,15 +109,11 @@ public final class AdsMediaSource implements MediaSource {
@Nullable private final Handler eventHandler;
@Nullable private final EventListener eventListener;
private final Handler mainHandler;
- private final ComponentListener componentListener;
private final Map> deferredMediaPeriodByAdMediaSource;
private final Timeline.Period period;
- private Handler playerHandler;
- private ExoPlayer player;
- private volatile boolean released;
-
// Accessed on the player thread.
+ private ComponentListener componentListener;
private Timeline contentTimeline;
private Object contentManifest;
private AdPlaybackState adPlaybackState;
@@ -193,7 +196,6 @@ public final class AdsMediaSource implements MediaSource {
this.eventHandler = eventHandler;
this.eventListener = eventListener;
mainHandler = new Handler(Looper.getMainLooper());
- componentListener = new ComponentListener();
deferredMediaPeriodByAdMediaSource = new HashMap<>();
period = new Timeline.Period();
adGroupMediaSources = new MediaSource[0][];
@@ -203,16 +205,12 @@ public final class AdsMediaSource implements MediaSource {
@Override
public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) {
+ super.prepareSource(player, isTopLevelSource, listener);
Assertions.checkArgument(isTopLevelSource);
+ final ComponentListener componentListener = new ComponentListener();
this.listener = listener;
- this.player = player;
- playerHandler = new Handler();
- contentMediaSource.prepareSource(player, false, new Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) {
- AdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest);
- }
- });
+ this.componentListener = componentListener;
+ prepareChildSource(new MediaPeriodId(/* periodIndex= */ 0), contentMediaSource);
mainHandler.post(new Runnable() {
@Override
public void run() {
@@ -221,26 +219,14 @@ public final class AdsMediaSource implements MediaSource {
});
}
- @Override
- public void maybeThrowSourceInfoRefreshError() throws IOException {
- contentMediaSource.maybeThrowSourceInfoRefreshError();
- for (MediaSource[] mediaSources : adGroupMediaSources) {
- for (MediaSource mediaSource : mediaSources) {
- if (mediaSource != null) {
- mediaSource.maybeThrowSourceInfoRefreshError();
- }
- }
- }
- }
-
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
if (adPlaybackState.adGroupCount > 0 && id.isAd()) {
- final int adGroupIndex = id.adGroupIndex;
- final int adIndexInAdGroup = id.adIndexInAdGroup;
+ int adGroupIndex = id.adGroupIndex;
+ int adIndexInAdGroup = id.adIndexInAdGroup;
if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) {
- Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup];
- final MediaSource adMediaSource =
+ Uri adUri = adPlaybackState.adGroups[id.adGroupIndex].uris[id.adIndexInAdGroup];
+ MediaSource adMediaSource =
adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener);
int oldAdCount = adGroupMediaSources[id.adGroupIndex].length;
if (adIndexInAdGroup >= oldAdCount) {
@@ -252,17 +238,16 @@ public final class AdsMediaSource implements MediaSource {
}
adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource;
deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList());
- adMediaSource.prepareSource(player, false, new MediaSource.Listener() {
- @Override
- public void onSourceInfoRefreshed(MediaSource source, Timeline timeline,
- @Nullable Object manifest) {
- onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline);
- }
- });
+ prepareChildSource(id, adMediaSource);
}
MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup];
DeferredMediaPeriod deferredMediaPeriod =
- new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator);
+ new DeferredMediaPeriod(
+ mediaSource,
+ new MediaPeriodId(/* periodIndex= */ 0, id.windowSequenceNumber),
+ allocator);
+ deferredMediaPeriod.setPrepareErrorListener(
+ new AdPrepareErrorListener(adGroupIndex, adIndexInAdGroup));
List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource);
if (mediaPeriods == null) {
deferredMediaPeriod.createPeriod();
@@ -281,20 +266,27 @@ public final class AdsMediaSource implements MediaSource {
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
- ((DeferredMediaPeriod) mediaPeriod).releasePeriod();
+ DeferredMediaPeriod deferredMediaPeriod = (DeferredMediaPeriod) mediaPeriod;
+ List mediaPeriods =
+ deferredMediaPeriodByAdMediaSource.get(deferredMediaPeriod.mediaSource);
+ if (mediaPeriods != null) {
+ mediaPeriods.remove(deferredMediaPeriod);
+ }
+ deferredMediaPeriod.releasePeriod();
}
@Override
public void releaseSource() {
- released = true;
- contentMediaSource.releaseSource();
- for (MediaSource[] mediaSources : adGroupMediaSources) {
- for (MediaSource mediaSource : mediaSources) {
- if (mediaSource != null) {
- mediaSource.releaseSource();
- }
- }
- }
+ super.releaseSource();
+ componentListener.release();
+ componentListener = null;
+ deferredMediaPeriodByAdMediaSource.clear();
+ contentTimeline = null;
+ contentManifest = null;
+ adPlaybackState = null;
+ adGroupMediaSources = new MediaSource[0][];
+ adDurationsUs = new long[0][];
+ listener = null;
mainHandler.post(new Runnable() {
@Override
public void run() {
@@ -303,6 +295,21 @@ public final class AdsMediaSource implements MediaSource {
});
}
+ @Override
+ protected void onChildSourceInfoRefreshed(
+ MediaPeriodId mediaPeriodId,
+ MediaSource mediaSource,
+ Timeline timeline,
+ @Nullable Object manifest) {
+ if (mediaPeriodId.isAd()) {
+ int adGroupIndex = mediaPeriodId.adGroupIndex;
+ int adIndexInAdGroup = mediaPeriodId.adIndexInAdGroup;
+ onAdSourceInfoRefreshed(mediaSource, adGroupIndex, adIndexInAdGroup, timeline);
+ } else {
+ onContentSourceInfoRefreshed(timeline, manifest);
+ }
+ }
+
// Internal methods.
private void onAdPlaybackState(AdPlaybackState adPlaybackState) {
@@ -316,20 +323,6 @@ public final class AdsMediaSource implements MediaSource {
maybeUpdateSourceInfo();
}
- private void onLoadError(final IOException error) {
- Log.w(TAG, "Ad load error", error);
- if (eventHandler != null && eventListener != null) {
- eventHandler.post(new Runnable() {
- @Override
- public void run() {
- if (!released) {
- eventListener.onAdLoadError(error);
- }
- }
- });
- }
- }
-
private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) {
contentTimeline = timeline;
contentManifest = manifest;
@@ -352,11 +345,11 @@ public final class AdsMediaSource implements MediaSource {
private void maybeUpdateSourceInfo() {
if (adPlaybackState != null && contentTimeline != null) {
- Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline
- : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs,
- adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts,
- adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs,
- adPlaybackState.contentDurationUs);
+ adPlaybackState = adPlaybackState.withAdDurationsUs(adDurationsUs);
+ Timeline timeline =
+ adPlaybackState.adGroupCount == 0
+ ? contentTimeline
+ : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
listener.onSourceInfoRefreshed(this, timeline, contentManifest);
}
}
@@ -364,6 +357,23 @@ public final class AdsMediaSource implements MediaSource {
/** Listener for component events. All methods are called on the main thread. */
private final class ComponentListener implements AdsLoader.EventListener {
+ private final Handler playerHandler;
+ private volatile boolean released;
+
+ /**
+ * Creates new listener which forwards ad playback states on the creating thread and all other
+ * events on the external event listener thread.
+ */
+ public ComponentListener() {
+ playerHandler = new Handler();
+ }
+
+ /** Releases the component listener. */
+ public void release() {
+ released = true;
+ playerHandler.removeCallbacksAndMessages(null);
+ }
+
@Override
public void onAdPlaybackState(final AdPlaybackState adPlaybackState) {
if (released) {
@@ -382,6 +392,9 @@ public final class AdsMediaSource implements MediaSource {
@Override
public void onAdClicked() {
+ if (released) {
+ return;
+ }
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
@@ -396,6 +409,9 @@ public final class AdsMediaSource implements MediaSource {
@Override
public void onAdTapped() {
+ if (released) {
+ return;
+ }
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
@@ -409,21 +425,63 @@ public final class AdsMediaSource implements MediaSource {
}
@Override
- public void onLoadError(final IOException error) {
+ public void onAdLoadError(final IOException error) {
if (released) {
return;
}
- playerHandler.post(new Runnable() {
- @Override
- public void run() {
- if (released) {
- return;
- }
- AdsMediaSource.this.onLoadError(error);
- }
- });
+ Log.w(TAG, "Ad load error", error);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ eventListener.onAdLoadError(error);
+ }
+ }
+ });
+ }
}
+ @Override
+ public void onInternalAdLoadError(final RuntimeException error) {
+ if (released) {
+ return;
+ }
+ Log.w(TAG, "Internal ad load error", error);
+ if (eventHandler != null && eventListener != null) {
+ eventHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ if (!released) {
+ eventListener.onInternalAdLoadError(error);
+ }
+ }
+ });
+ }
+ }
}
+ private final class AdPrepareErrorListener implements DeferredMediaPeriod.PrepareErrorListener {
+
+ private final int adGroupIndex;
+ private final int adIndexInAdGroup;
+
+ public AdPrepareErrorListener(int adGroupIndex, int adIndexInAdGroup) {
+ this.adGroupIndex = adGroupIndex;
+ this.adIndexInAdGroup = adIndexInAdGroup;
+ }
+
+ @Override
+ public void onPrepareError(final IOException exception) {
+ mainHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ adsLoader.handlePrepareError(adGroupIndex, adIndexInAdGroup, exception);
+ }
+ });
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
index 0a04c9ab4b..ec0d6cb2fe 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java
@@ -25,54 +25,32 @@ import com.google.android.exoplayer2.util.Assertions;
*/
/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline {
- private final long[] adGroupTimesUs;
- private final int[] adCounts;
- private final int[] adsLoadedCounts;
- private final int[] adsPlayedCounts;
- private final long[][] adDurationsUs;
- private final long adResumePositionUs;
- private final long contentDurationUs;
+ private final AdPlaybackState adPlaybackState;
/**
- * Creates a new timeline with a single period containing the specified ads.
+ * Creates a new timeline with a single period containing ads.
*
* @param contentTimeline The timeline of the content alongside which ads will be played. It must
* have one window and one period.
- * @param adGroupTimesUs The times of ad groups relative to the start of the period, in
- * microseconds. A final element with the value {@link C#TIME_END_OF_SOURCE} indicates that
- * the period has a postroll ad.
- * @param adCounts The number of ads in each ad group. An element may be {@link C#LENGTH_UNSET}
- * if the number of ads is not yet known.
- * @param adsLoadedCounts The number of ads loaded so far in each ad group.
- * @param adsPlayedCounts The number of ads played so far in each ad group.
- * @param adDurationsUs The duration of each ad in each ad group, in microseconds. An element
- * may be {@link C#TIME_UNSET} if the duration is not yet known.
- * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin
- * playback, in microseconds.
- * @param contentDurationUs The content duration in microseconds, if known. {@link C#TIME_UNSET}
- * otherwise.
+ * @param adPlaybackState The state of the period's ads.
*/
- public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts,
- int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs,
- long contentDurationUs) {
+ public SinglePeriodAdTimeline(Timeline contentTimeline, AdPlaybackState adPlaybackState) {
super(contentTimeline);
Assertions.checkState(contentTimeline.getPeriodCount() == 1);
Assertions.checkState(contentTimeline.getWindowCount() == 1);
- this.adGroupTimesUs = adGroupTimesUs;
- this.adCounts = adCounts;
- this.adsLoadedCounts = adsLoadedCounts;
- this.adsPlayedCounts = adsPlayedCounts;
- this.adDurationsUs = adDurationsUs;
- this.adResumePositionUs = adResumePositionUs;
- this.contentDurationUs = contentDurationUs;
+ this.adPlaybackState = adPlaybackState;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
timeline.getPeriod(periodIndex, period, setIds);
- period.set(period.id, period.uid, period.windowIndex, period.durationUs,
- period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts,
- adDurationsUs, adResumePositionUs);
+ period.set(
+ period.id,
+ period.uid,
+ period.windowIndex,
+ period.durationUs,
+ period.getPositionInWindowUs(),
+ adPlaybackState);
return period;
}
@@ -81,7 +59,7 @@ import com.google.android.exoplayer2.util.Assertions;
long defaultPositionProjectionUs) {
window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs);
if (window.durationUs == C.TIME_UNSET) {
- window.durationUs = contentDurationUs;
+ window.durationUs = adPlaybackState.contentDurationUs;
}
return window;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
index 62c07ee248..c8ebc02434 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/BaseMediaChunk.java
@@ -37,9 +37,15 @@ public abstract class BaseMediaChunk extends MediaChunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
*/
- public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
- int chunkIndex) {
+ public BaseMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
endTimeUs, chunkIndex);
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
index fa95269690..7096c84c5e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSampleStream.java
@@ -15,10 +15,12 @@
*/
package com.google.android.exoplayer2.source.chunk;
+import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.SampleQueue;
@@ -27,9 +29,10 @@ import com.google.android.exoplayer2.source.SequenceableLoader;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.util.Assertions;
+import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
import java.util.List;
/**
@@ -39,10 +42,23 @@ import java.util.List;
public class ChunkSampleStream implements SampleStream, SequenceableLoader,
Loader.Callback, Loader.ReleaseCallback {
+ /** A callback to be notified when a sample stream has finished being released. */
+ public interface ReleaseCallback {
+
+ /**
+ * Called when the {@link ChunkSampleStream} has finished being released.
+ *
+ * @param chunkSampleStream The released sample stream.
+ */
+ void onSampleStreamReleased(ChunkSampleStream chunkSampleStream);
+ }
+
private static final String TAG = "ChunkSampleStream";
- private final int primaryTrackType;
+ public final int primaryTrackType;
+
private final int[] embeddedTrackTypes;
+ private final Format[] embeddedTrackFormats;
private final boolean[] embeddedTracksSelected;
private final T chunkSource;
private final SequenceableLoader.Callback> callback;
@@ -50,21 +66,24 @@ public class ChunkSampleStream implements SampleStream, S
private final int minLoadableRetryCount;
private final Loader loader;
private final ChunkHolder nextChunkHolder;
- private final LinkedList mediaChunks;
+ private final ArrayList mediaChunks;
private final List readOnlyMediaChunks;
private final SampleQueue primarySampleQueue;
private final SampleQueue[] embeddedSampleQueues;
private final BaseMediaChunkOutput mediaChunkOutput;
private Format primaryDownstreamTrackFormat;
+ private @Nullable ReleaseCallback releaseCallback;
private long pendingResetPositionUs;
- /* package */ long lastSeekPositionUs;
+ private long lastSeekPositionUs;
+ /* package */ long decodeOnlyUntilPositionUs;
/* package */ boolean loadingFinished;
/**
- * @param primaryTrackType The type of the primary track. One of the {@link C}
- * {@code TRACK_TYPE_*} constants.
+ * @param primaryTrackType The type of the primary track. One of the {@link C} {@code
+ * TRACK_TYPE_*} constants.
* @param embeddedTrackTypes The types of any embedded tracks, or null.
+ * @param embeddedTrackFormats The formats of the embedded tracks, or null.
* @param chunkSource A {@link ChunkSource} from which chunks to load are obtained.
* @param callback An {@link Callback} for the stream.
* @param allocator An {@link Allocator} from which allocations can be obtained.
@@ -73,18 +92,26 @@ public class ChunkSampleStream implements SampleStream, S
* before propagating an error.
* @param eventDispatcher A dispatcher to notify of events.
*/
- public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource,
- Callback> callback, Allocator allocator, long positionUs,
- int minLoadableRetryCount, EventDispatcher eventDispatcher) {
+ public ChunkSampleStream(
+ int primaryTrackType,
+ int[] embeddedTrackTypes,
+ Format[] embeddedTrackFormats,
+ T chunkSource,
+ Callback> callback,
+ Allocator allocator,
+ long positionUs,
+ int minLoadableRetryCount,
+ EventDispatcher eventDispatcher) {
this.primaryTrackType = primaryTrackType;
this.embeddedTrackTypes = embeddedTrackTypes;
+ this.embeddedTrackFormats = embeddedTrackFormats;
this.chunkSource = chunkSource;
this.callback = callback;
this.eventDispatcher = eventDispatcher;
this.minLoadableRetryCount = minLoadableRetryCount;
loader = new Loader("Loader:ChunkSampleStream");
nextChunkHolder = new ChunkHolder();
- mediaChunks = new LinkedList<>();
+ mediaChunks = new ArrayList<>();
readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length;
@@ -109,20 +136,23 @@ public class ChunkSampleStream implements SampleStream, S
lastSeekPositionUs = positionUs;
}
- // TODO: Generalize this method to also discard from the primary sample queue and stop discarding
- // from this queue in readData and skipData. This will cause samples to be kept in the queue until
- // they've been rendered, rather than being discarded as soon as they're read by the renderer.
- // This will make in-buffer seeks more likely when seeking slightly forward from the current
- // position. This change will need handling with care, in particular when considering removal of
- // chunks from the front of the mediaChunks list.
/**
- * Discards buffered media for embedded tracks, up to the specified position.
+ * Discards buffered media up to the specified position.
*
* @param positionUs The position to discard up to, in microseconds.
+ * @param toKeyframe If true then for each track discards samples up to the keyframe before or at
+ * the specified position, rather than any sample before or at that position.
*/
- public void discardEmbeddedTracksTo(long positionUs) {
- for (int i = 0; i < embeddedSampleQueues.length; i++) {
- embeddedSampleQueues[i].discardTo(positionUs, true, embeddedTracksSelected[i]);
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ int oldFirstIndex = primarySampleQueue.getFirstIndex();
+ primarySampleQueue.discardTo(positionUs, toKeyframe, true);
+ int newFirstIndex = primarySampleQueue.getFirstIndex();
+ if (newFirstIndex > oldFirstIndex) {
+ long discardToUs = primarySampleQueue.getFirstTimestampUs();
+ for (int i = 0; i < embeddedSampleQueues.length; i++) {
+ embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]);
+ }
+ discardDownstreamMediaChunks(newFirstIndex);
}
}
@@ -171,7 +201,7 @@ public class ChunkSampleStream implements SampleStream, S
return pendingResetPositionUs;
} else {
long bufferedPositionUs = lastSeekPositionUs;
- BaseMediaChunk lastMediaChunk = mediaChunks.getLast();
+ BaseMediaChunk lastMediaChunk = getLastMediaChunk();
BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk
: mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null;
if (lastCompletedMediaChunk != null) {
@@ -181,6 +211,18 @@ public class ChunkSampleStream implements SampleStream, S
}
}
+ /**
+ * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
+ * as sync points.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed.
+ * @return The adjusted seek position, in microseconds.
+ */
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
/**
* Seeks to the specified position in microseconds.
*
@@ -188,16 +230,49 @@ public class ChunkSampleStream implements SampleStream, S
*/
public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs;
- // If we're not pending a reset, see if we can seek within the primary sample queue.
- boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true,
- positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED);
+ primarySampleQueue.rewind();
+
+ // See if we can seek within the primary sample queue.
+ boolean seekInsideBuffer;
+ if (isPendingReset()) {
+ seekInsideBuffer = false;
+ } else {
+ // Detect whether the seek is to the start of a chunk that's at least partially buffered.
+ BaseMediaChunk seekToMediaChunk = null;
+ for (int i = 0; i < mediaChunks.size(); i++) {
+ BaseMediaChunk mediaChunk = mediaChunks.get(i);
+ long mediaChunkStartTimeUs = mediaChunk.startTimeUs;
+ if (mediaChunkStartTimeUs == positionUs) {
+ seekToMediaChunk = mediaChunk;
+ break;
+ } else if (mediaChunkStartTimeUs > positionUs) {
+ // We're not going to find a chunk with a matching start time.
+ break;
+ }
+ }
+ if (seekToMediaChunk != null) {
+ // When seeking to the start of a chunk we use the index of the first sample in the chunk
+ // rather than the seek position. This ensures we seek to the keyframe at the start of the
+ // chunk even if the sample timestamps are slightly offset from the chunk start times.
+ seekInsideBuffer =
+ primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0));
+ decodeOnlyUntilPositionUs = Long.MIN_VALUE;
+ } else {
+ seekInsideBuffer =
+ primarySampleQueue.advanceTo(
+ positionUs,
+ /* toKeyframe= */ true,
+ /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs())
+ != SampleQueue.ADVANCE_FAILED;
+ decodeOnlyUntilPositionUs = lastSeekPositionUs;
+ }
+ }
+
if (seekInsideBuffer) {
- // We succeeded. Discard samples and corresponding chunks prior to the seek position.
- discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
- primarySampleQueue.discardToRead();
+ // We succeeded. Advance the embedded sample queues to the seek position.
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.rewind();
- embeddedSampleQueue.discardTo(positionUs, true, false);
+ embeddedSampleQueue.advanceTo(positionUs, true, false);
}
} else {
// We failed, and need to restart.
@@ -217,18 +292,31 @@ public class ChunkSampleStream implements SampleStream, S
/**
* Releases the stream.
- *
- * This method should be called when the stream is no longer required.
+ *
+ *
This method should be called when the stream is no longer required. Either this method or
+ * {@link #release(ReleaseCallback)} can be used to release this stream.
*/
public void release() {
- boolean releasedSynchronously = loader.release(this);
- if (!releasedSynchronously) {
- // Discard as much as we can synchronously.
- primarySampleQueue.discardToEnd();
- for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
- embeddedSampleQueue.discardToEnd();
- }
+ release(null);
+ }
+
+ /**
+ * Releases the stream.
+ *
+ *
This method should be called when the stream is no longer required. Either this method or
+ * {@link #release()} can be used to release this stream.
+ *
+ * @param callback An optional callback to be called on the loading thread once the loader has
+ * been released.
+ */
+ public void release(@Nullable ReleaseCallback callback) {
+ this.releaseCallback = callback;
+ // Discard as much as we can synchronously.
+ primarySampleQueue.discardToEnd();
+ for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
+ embeddedSampleQueue.discardToEnd();
}
+ loader.release(this);
}
@Override
@@ -237,6 +325,9 @@ public class ChunkSampleStream implements SampleStream, S
for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) {
embeddedSampleQueue.reset();
}
+ if (releaseCallback != null) {
+ releaseCallback.onSampleStreamReleased(this);
+ }
}
// SampleStream implementation.
@@ -260,11 +351,11 @@ public class ChunkSampleStream implements SampleStream, S
if (isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
- discardDownstreamMediaChunks(primarySampleQueue.getReadIndex());
- int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
- lastSeekPositionUs);
+ int result =
+ primarySampleQueue.read(
+ formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);
if (result == C.RESULT_BUFFER_READ) {
- primarySampleQueue.discardToRead();
+ maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), 1);
}
return result;
}
@@ -284,7 +375,7 @@ public class ChunkSampleStream implements SampleStream, S
}
}
if (skipCount > 0) {
- primarySampleQueue.discardToRead();
+ maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), skipCount);
}
return skipCount;
}
@@ -322,7 +413,9 @@ public class ChunkSampleStream implements SampleStream, S
IOException error) {
long bytesLoaded = loadable.bytesLoaded();
boolean isMediaChunk = isMediaChunk(loadable);
- boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk();
+ int lastChunkIndex = mediaChunks.size() - 1;
+ boolean cancelable =
+ bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex);
boolean canceled = false;
if (chunkSource.onChunkLoadError(loadable, cancelable, error)) {
if (!cancelable) {
@@ -330,12 +423,8 @@ public class ChunkSampleStream implements SampleStream, S
} else {
canceled = true;
if (isMediaChunk) {
- BaseMediaChunk removed = mediaChunks.removeLast();
+ BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex);
Assertions.checkState(removed == loadable);
- primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
- for (int i = 0; i < embeddedSampleQueues.length; i++) {
- embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
- }
if (mediaChunks.isEmpty()) {
pendingResetPositionUs = lastSeekPositionUs;
}
@@ -362,13 +451,14 @@ public class ChunkSampleStream implements SampleStream, S
return false;
}
+ boolean pendingReset = isPendingReset();
MediaChunk previousChunk;
long loadPositionUs;
- if (isPendingReset()) {
+ if (pendingReset) {
previousChunk = null;
loadPositionUs = pendingResetPositionUs;
} else {
- previousChunk = mediaChunks.getLast();
+ previousChunk = getLastMediaChunk();
loadPositionUs = previousChunk.endTimeUs;
}
chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder);
@@ -387,8 +477,13 @@ public class ChunkSampleStream implements SampleStream, S
}
if (isMediaChunk(loadable)) {
- pendingResetPositionUs = C.TIME_UNSET;
BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable;
+ if (pendingReset) {
+ boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs;
+ // Only enable setting of the decode only flag if we're not resetting to a chunk boundary.
+ decodeOnlyUntilPositionUs = resetToMediaChunk ? Long.MIN_VALUE : pendingResetPositionUs;
+ pendingResetPositionUs = C.TIME_UNSET;
+ }
mediaChunk.init(mediaChunkOutput);
mediaChunks.add(mediaChunk);
}
@@ -404,38 +499,56 @@ public class ChunkSampleStream implements SampleStream, S
if (isPendingReset()) {
return pendingResetPositionUs;
} else {
- return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs;
+ return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs;
}
}
- // Internal methods
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ if (loader.isLoading() || isPendingReset()) {
+ return;
+ }
- // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming.
- /**
- * Discards media chunks from the back of the buffer if conditions have changed such that it's
- * preferable to re-buffer the media at a different quality.
- *
- * @param positionUs The current playback position in microseconds.
- */
- private void maybeDiscardUpstream(long positionUs) {
- int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
- discardUpstreamMediaChunks(Math.max(1, queueSize));
+ int currentQueueSize = mediaChunks.size();
+ int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks);
+ if (currentQueueSize <= preferredQueueSize) {
+ return;
+ }
+
+ int newQueueSize = currentQueueSize;
+ for (int i = preferredQueueSize; i < currentQueueSize; i++) {
+ if (!haveReadFromMediaChunk(i)) {
+ newQueueSize = i;
+ break;
+ }
+ }
+ if (newQueueSize == currentQueueSize) {
+ return;
+ }
+
+ long endTimeUs = getLastMediaChunk().endTimeUs;
+ BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize);
+ if (mediaChunks.isEmpty()) {
+ pendingResetPositionUs = lastSeekPositionUs;
+ }
+ loadingFinished = false;
+ eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs);
}
+ // Internal methods
+
private boolean isMediaChunk(Chunk chunk) {
return chunk instanceof BaseMediaChunk;
}
- /**
- * Returns whether samples have been read from {@code mediaChunks.getLast()}.
- */
- private boolean haveReadFromLastMediaChunk() {
- BaseMediaChunk lastChunk = mediaChunks.getLast();
- if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) {
+ /** Returns whether samples have been read from media chunk at given index. */
+ private boolean haveReadFromMediaChunk(int mediaChunkIndex) {
+ BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex);
+ if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) {
return true;
}
for (int i = 0; i < embeddedSampleQueues.length; i++) {
- if (embeddedSampleQueues[i].getReadIndex() > lastChunk.getFirstSampleIndex(i + 1)) {
+ if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) {
return true;
}
}
@@ -446,47 +559,68 @@ public class ChunkSampleStream implements SampleStream, S
return pendingResetPositionUs != C.TIME_UNSET;
}
- private void discardDownstreamMediaChunks(int primaryStreamReadIndex) {
- if (!mediaChunks.isEmpty()) {
- while (mediaChunks.size() > 1
- && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) {
- mediaChunks.removeFirst();
- }
- BaseMediaChunk currentChunk = mediaChunks.getFirst();
- Format trackFormat = currentChunk.trackFormat;
- if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
- eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
- currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
- currentChunk.startTimeUs);
- }
- primaryDownstreamTrackFormat = trackFormat;
+ private void discardDownstreamMediaChunks(int discardToPrimaryStreamIndex) {
+ int discardToMediaChunkIndex =
+ primaryStreamIndexToMediaChunkIndex(discardToPrimaryStreamIndex, /* minChunkIndex= */ 0);
+ if (discardToMediaChunkIndex > 0) {
+ Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex);
}
}
+ private void maybeNotifyPrimaryTrackFormatChanged(int toPrimaryStreamReadIndex, int readCount) {
+ int fromMediaChunkIndex = primaryStreamIndexToMediaChunkIndex(
+ toPrimaryStreamReadIndex - readCount, /* minChunkIndex= */ 0);
+ int toMediaChunkIndexInclusive = readCount == 1 ? fromMediaChunkIndex
+ : primaryStreamIndexToMediaChunkIndex(toPrimaryStreamReadIndex - 1,
+ /* minChunkIndex= */ fromMediaChunkIndex);
+ for (int i = fromMediaChunkIndex; i <= toMediaChunkIndexInclusive; i++) {
+ maybeNotifyPrimaryTrackFormatChanged(i);
+ }
+ }
+
+ private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) {
+ BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex);
+ Format trackFormat = currentChunk.trackFormat;
+ if (!trackFormat.equals(primaryDownstreamTrackFormat)) {
+ eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat,
+ currentChunk.trackSelectionReason, currentChunk.trackSelectionData,
+ currentChunk.startTimeUs);
+ }
+ primaryDownstreamTrackFormat = trackFormat;
+ }
+
/**
- * Discard upstream media chunks until the queue length is equal to the length specified.
- *
- * @param queueLength The desired length of the queue.
- * @return Whether chunks were discarded.
+ * Returns media chunk index for primary stream sample index. May be -1 if the list of media
+ * chunks is empty or the requested index is less than the first index in the first media chunk.
*/
- private boolean discardUpstreamMediaChunks(int queueLength) {
- if (mediaChunks.size() <= queueLength) {
- return false;
+ private int primaryStreamIndexToMediaChunkIndex(int primaryStreamIndex, int minChunkIndex) {
+ for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) {
+ if (mediaChunks.get(i).getFirstSampleIndex(0) > primaryStreamIndex) {
+ return i - 1;
+ }
}
- BaseMediaChunk removed;
- long startTimeUs;
- long endTimeUs = mediaChunks.getLast().endTimeUs;
- do {
- removed = mediaChunks.removeLast();
- startTimeUs = removed.startTimeUs;
- } while (mediaChunks.size() > queueLength);
- primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0));
+ return mediaChunks.size() - 1;
+ }
+
+ private BaseMediaChunk getLastMediaChunk() {
+ return mediaChunks.get(mediaChunks.size() - 1);
+ }
+
+ /**
+ * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample
+ * queues.
+ *
+ * @param chunkIndex The index of the first chunk to discard.
+ * @return The chunk at given index.
+ */
+ private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) {
+ BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex);
+ Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size());
+ primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0));
for (int i = 0; i < embeddedSampleQueues.length; i++) {
- embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1));
+ embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1));
}
- loadingFinished = false;
- eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs);
- return true;
+ return firstRemovedChunk;
}
/**
@@ -499,6 +633,8 @@ public class ChunkSampleStream implements SampleStream, S
private final SampleQueue sampleQueue;
private final int index;
+ private boolean formatNotificationSent;
+
public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) {
this.parent = parent;
this.sampleQueue = sampleQueue;
@@ -512,12 +648,19 @@ public class ChunkSampleStream implements SampleStream, S
@Override
public int skipData(long positionUs) {
+ int skipCount;
if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) {
- return sampleQueue.advanceToEnd();
+ skipCount = sampleQueue.advanceToEnd();
} else {
- int skipCount = sampleQueue.advanceTo(positionUs, true, true);
- return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount;
+ skipCount = sampleQueue.advanceTo(positionUs, true, true);
+ if (skipCount == SampleQueue.ADVANCE_FAILED) {
+ skipCount = 0;
+ }
}
+ if (skipCount > 0) {
+ maybeNotifyTrackFormatChanged();
+ }
+ return skipCount;
}
@Override
@@ -531,8 +674,13 @@ public class ChunkSampleStream implements SampleStream, S
if (isPendingReset()) {
return C.RESULT_NOTHING_READ;
}
- return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished,
- lastSeekPositionUs);
+ int result =
+ sampleQueue.read(
+ formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs);
+ if (result == C.RESULT_BUFFER_READ) {
+ maybeNotifyTrackFormatChanged();
+ }
+ return result;
}
public void release() {
@@ -540,6 +688,17 @@ public class ChunkSampleStream implements SampleStream, S
embeddedTracksSelected[index] = false;
}
+ private void maybeNotifyTrackFormatChanged() {
+ if (!formatNotificationSent) {
+ eventDispatcher.downstreamFormatChanged(
+ embeddedTrackTypes[index],
+ embeddedTrackFormats[index],
+ C.SELECTION_REASON_UNKNOWN,
+ /* trackSelectionData= */ null,
+ lastSeekPositionUs);
+ formatNotificationSent = true;
+ }
+ }
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java
index b04dc7cbdb..568461c206 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ChunkSource.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.chunk;
+import com.google.android.exoplayer2.SeekParameters;
import java.io.IOException;
import java.util.List;
@@ -23,6 +24,16 @@ import java.util.List;
*/
public interface ChunkSource {
+ /**
+ * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used
+ * as sync points.
+ *
+ * @param positionUs The seek position in microseconds.
+ * @param seekParameters Parameters that control how the seek is performed.
+ * @return The adjusted seek position, in microseconds.
+ */
+ long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters);
+
/**
* If the source is currently having difficulty providing chunks, then this method throws the
* underlying error. Otherwise does nothing.
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
index cc39c88fd0..b43c69b63a 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/ContainerMediaChunk.java
@@ -53,9 +53,18 @@ public class ContainerMediaChunk extends BaseMediaChunk {
* @param sampleOffsetUs An offset to add to the sample timestamps parsed by the extractor.
* @param extractorWrapper A wrapped extractor to use for parsing the data.
*/
- public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
- int chunkIndex, int chunkCount, long sampleOffsetUs, ChunkExtractorWrapper extractorWrapper) {
+ public ContainerMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex,
+ int chunkCount,
+ long sampleOffsetUs,
+ ChunkExtractorWrapper extractorWrapper) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
endTimeUs, chunkIndex);
this.chunkCount = chunkCount;
@@ -64,7 +73,7 @@ public class ContainerMediaChunk extends BaseMediaChunk {
}
@Override
- public int getNextChunkIndex() {
+ public long getNextChunkIndex() {
return chunkIndex + chunkCount;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java
index 3a02884fff..d313a8cb81 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/MediaChunk.java
@@ -26,10 +26,8 @@ import com.google.android.exoplayer2.util.Assertions;
*/
public abstract class MediaChunk extends Chunk {
- /**
- * The chunk index.
- */
- public final int chunkIndex;
+ /** The chunk index. */
+ public final long chunkIndex;
/**
* @param dataSource The source from which the data should be loaded.
@@ -41,19 +39,23 @@ public abstract class MediaChunk extends Chunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk.
*/
- public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
- int chunkIndex) {
+ public MediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex) {
super(dataSource, dataSpec, C.DATA_TYPE_MEDIA, trackFormat, trackSelectionReason,
trackSelectionData, startTimeUs, endTimeUs);
Assertions.checkNotNull(trackFormat);
this.chunkIndex = chunkIndex;
}
- /**
- * Returns the next chunk index.
- */
- public int getNextChunkIndex() {
+ /** Returns the next chunk index. */
+ public long getNextChunkIndex() {
return chunkIndex + 1;
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
index 02cf7dfd55..87a90bc285 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/source/chunk/SingleSampleMediaChunk.java
@@ -50,9 +50,17 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
* constants.
* @param sampleFormat The {@link Format} of the sample in the chunk.
*/
- public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format trackFormat,
- int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs,
- int chunkIndex, int trackType, Format sampleFormat) {
+ public SingleSampleMediaChunk(
+ DataSource dataSource,
+ DataSpec dataSpec,
+ Format trackFormat,
+ int trackSelectionReason,
+ Object trackSelectionData,
+ long startTimeUs,
+ long endTimeUs,
+ long chunkIndex,
+ int trackType,
+ Format sampleFormat) {
super(dataSource, dataSpec, trackFormat, trackSelectionReason, trackSelectionData, startTimeUs,
endTimeUs, chunkIndex);
this.trackType = trackType;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
index 6955f775dd..997f750b61 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SimpleSubtitleDecoder.java
@@ -57,6 +57,11 @@ public abstract class SimpleSubtitleDecoder extends
return new SimpleSubtitleOutputBuffer(this);
}
+ @Override
+ protected final SubtitleDecoderException createUnexpectedDecodeException(Throwable error) {
+ return new SubtitleDecoderException("Unexpected decode error", error);
+ }
+
@Override
protected final void releaseOutputBuffer(SubtitleOutputBuffer buffer) {
super.releaseOutputBuffer(buffer);
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
index 6a9b83a015..139e403844 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java
@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.cea.Cea608Decoder;
import com.google.android.exoplayer2.text.cea.Cea708Decoder;
import com.google.android.exoplayer2.text.dvb.DvbDecoder;
+import com.google.android.exoplayer2.text.pgs.PgsDecoder;
import com.google.android.exoplayer2.text.ssa.SsaDecoder;
import com.google.android.exoplayer2.text.subrip.SubripDecoder;
import com.google.android.exoplayer2.text.ttml.TtmlDecoder;
@@ -52,64 +53,69 @@ public interface SubtitleDecoderFactory {
/**
* Default {@link SubtitleDecoderFactory} implementation.
- *
- * The formats supported by this factory are:
+ *
+ *
The formats supported by this factory are:
+ *
*
- * - WebVTT ({@link WebvttDecoder})
- * - WebVTT (MP4) ({@link Mp4WebvttDecoder})
- * - TTML ({@link TtmlDecoder})
- * - SubRip ({@link SubripDecoder})
- * - SSA/ASS ({@link SsaDecoder})
- * - TX3G ({@link Tx3gDecoder})
- * - Cea608 ({@link Cea608Decoder})
- * - Cea708 ({@link Cea708Decoder})
- * - DVB ({@link DvbDecoder})
+ * - WebVTT ({@link WebvttDecoder})
+ *
- WebVTT (MP4) ({@link Mp4WebvttDecoder})
+ *
- TTML ({@link TtmlDecoder})
+ *
- SubRip ({@link SubripDecoder})
+ *
- SSA/ASS ({@link SsaDecoder})
+ *
- TX3G ({@link Tx3gDecoder})
+ *
- Cea608 ({@link Cea608Decoder})
+ *
- Cea708 ({@link Cea708Decoder})
+ *
- DVB ({@link DvbDecoder})
+ *
- PGS ({@link PgsDecoder})
*
*/
- SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() {
+ SubtitleDecoderFactory DEFAULT =
+ new SubtitleDecoderFactory() {
- @Override
- public boolean supportsFormat(Format format) {
- String mimeType = format.sampleMimeType;
- return MimeTypes.TEXT_VTT.equals(mimeType)
- || MimeTypes.TEXT_SSA.equals(mimeType)
- || MimeTypes.APPLICATION_TTML.equals(mimeType)
- || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
- || MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
- || MimeTypes.APPLICATION_TX3G.equals(mimeType)
- || MimeTypes.APPLICATION_CEA608.equals(mimeType)
- || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType)
- || MimeTypes.APPLICATION_CEA708.equals(mimeType)
- || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType);
- }
-
- @Override
- public SubtitleDecoder createDecoder(Format format) {
- switch (format.sampleMimeType) {
- case MimeTypes.TEXT_VTT:
- return new WebvttDecoder();
- case MimeTypes.TEXT_SSA:
- return new SsaDecoder(format.initializationData);
- case MimeTypes.APPLICATION_MP4VTT:
- return new Mp4WebvttDecoder();
- case MimeTypes.APPLICATION_TTML:
- return new TtmlDecoder();
- case MimeTypes.APPLICATION_SUBRIP:
- return new SubripDecoder();
- case MimeTypes.APPLICATION_TX3G:
- return new Tx3gDecoder(format.initializationData);
- case MimeTypes.APPLICATION_CEA608:
- case MimeTypes.APPLICATION_MP4CEA608:
- return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel);
- case MimeTypes.APPLICATION_CEA708:
- return new Cea708Decoder(format.accessibilityChannel);
- case MimeTypes.APPLICATION_DVBSUBS:
- return new DvbDecoder(format.initializationData);
- default:
- throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
- }
- }
-
- };
+ @Override
+ public boolean supportsFormat(Format format) {
+ String mimeType = format.sampleMimeType;
+ return MimeTypes.TEXT_VTT.equals(mimeType)
+ || MimeTypes.TEXT_SSA.equals(mimeType)
+ || MimeTypes.APPLICATION_TTML.equals(mimeType)
+ || MimeTypes.APPLICATION_MP4VTT.equals(mimeType)
+ || MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
+ || MimeTypes.APPLICATION_TX3G.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA608.equals(mimeType)
+ || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType)
+ || MimeTypes.APPLICATION_CEA708.equals(mimeType)
+ || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)
+ || MimeTypes.APPLICATION_PGS.equals(mimeType);
+ }
+ @Override
+ public SubtitleDecoder createDecoder(Format format) {
+ switch (format.sampleMimeType) {
+ case MimeTypes.TEXT_VTT:
+ return new WebvttDecoder();
+ case MimeTypes.TEXT_SSA:
+ return new SsaDecoder(format.initializationData);
+ case MimeTypes.APPLICATION_MP4VTT:
+ return new Mp4WebvttDecoder();
+ case MimeTypes.APPLICATION_TTML:
+ return new TtmlDecoder();
+ case MimeTypes.APPLICATION_SUBRIP:
+ return new SubripDecoder();
+ case MimeTypes.APPLICATION_TX3G:
+ return new Tx3gDecoder(format.initializationData);
+ case MimeTypes.APPLICATION_CEA608:
+ case MimeTypes.APPLICATION_MP4CEA608:
+ return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel);
+ case MimeTypes.APPLICATION_CEA708:
+ return new Cea708Decoder(format.accessibilityChannel);
+ case MimeTypes.APPLICATION_DVBSUBS:
+ return new DvbDecoder(format.initializationData);
+ case MimeTypes.APPLICATION_PGS:
+ return new PgsDecoder();
+ default:
+ throw new IllegalArgumentException(
+ "Attempted to create decoder for unsupported format");
+ }
+ }
+ };
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
index 4b3b61bddf..9866517a58 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java
@@ -15,15 +15,11 @@
*/
package com.google.android.exoplayer2.text;
-import android.support.annotation.NonNull;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
-/**
- * A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}.
- */
-public final class SubtitleInputBuffer extends DecoderInputBuffer
- implements Comparable {
+/** A {@link DecoderInputBuffer} for a {@link SubtitleDecoder}. */
+public class SubtitleInputBuffer extends DecoderInputBuffer {
/**
* An offset that must be added to the subtitle's event times after it's been decoded, or
@@ -35,16 +31,4 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
- @Override
- public int compareTo(@NonNull SubtitleInputBuffer other) {
- if (isEndOfStream() != other.isEndOfStream()) {
- return isEndOfStream() ? 1 : -1;
- }
- long delta = timeUs - other.timeUs;
- if (delta == 0) {
- return 0;
- }
- return delta > 0 ? 1 : -1;
- }
-
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
index bb13a7d143..07a55f1a40 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaDecoder.java
@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.text.cea;
+import android.support.annotation.NonNull;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Subtitle;
@@ -34,21 +35,22 @@ import java.util.PriorityQueue;
private static final int NUM_INPUT_BUFFERS = 10;
private static final int NUM_OUTPUT_BUFFERS = 2;
- private final LinkedList availableInputBuffers;
+ private final LinkedList availableInputBuffers;
private final LinkedList availableOutputBuffers;
- private final PriorityQueue queuedInputBuffers;
+ private final PriorityQueue queuedInputBuffers;
- private SubtitleInputBuffer dequeuedInputBuffer;
+ private CeaInputBuffer dequeuedInputBuffer;
private long playbackPositionUs;
+ private long queuedInputBufferCount;
public CeaDecoder() {
availableInputBuffers = new LinkedList<>();
for (int i = 0; i < NUM_INPUT_BUFFERS; i++) {
- availableInputBuffers.add(new SubtitleInputBuffer());
+ availableInputBuffers.add(new CeaInputBuffer());
}
availableOutputBuffers = new LinkedList<>();
for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) {
- availableOutputBuffers.add(new CeaOutputBuffer(this));
+ availableOutputBuffers.add(new CeaOutputBuffer());
}
queuedInputBuffers = new PriorityQueue<>();
}
@@ -77,9 +79,10 @@ import java.util.PriorityQueue;
if (inputBuffer.isDecodeOnly()) {
// We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow
// for decoding to begin mid-stream.
- releaseInputBuffer(inputBuffer);
+ releaseInputBuffer(dequeuedInputBuffer);
} else {
- queuedInputBuffers.add(inputBuffer);
+ dequeuedInputBuffer.queuedInputBufferCount = queuedInputBufferCount++;
+ queuedInputBuffers.add(dequeuedInputBuffer);
}
dequeuedInputBuffer = null;
}
@@ -94,7 +97,7 @@ import java.util.PriorityQueue;
// be deferred until they would be applicable
while (!queuedInputBuffers.isEmpty()
&& queuedInputBuffers.peek().timeUs <= playbackPositionUs) {
- SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll();
+ CeaInputBuffer inputBuffer = queuedInputBuffers.poll();
// If the input buffer indicates we've reached the end of the stream, we can
// return immediately with an output buffer propagating that
@@ -126,7 +129,7 @@ import java.util.PriorityQueue;
return null;
}
- private void releaseInputBuffer(SubtitleInputBuffer inputBuffer) {
+ private void releaseInputBuffer(CeaInputBuffer inputBuffer) {
inputBuffer.clear();
availableInputBuffers.add(inputBuffer);
}
@@ -138,6 +141,7 @@ import java.util.PriorityQueue;
@Override
public void flush() {
+ queuedInputBufferCount = 0;
playbackPositionUs = 0;
while (!queuedInputBuffers.isEmpty()) {
releaseInputBuffer(queuedInputBuffers.poll());
@@ -169,4 +173,32 @@ import java.util.PriorityQueue;
*/
protected abstract void decode(SubtitleInputBuffer inputBuffer);
+ private static final class CeaInputBuffer extends SubtitleInputBuffer
+ implements Comparable {
+
+ private long queuedInputBufferCount;
+
+ @Override
+ public int compareTo(@NonNull CeaInputBuffer other) {
+ if (isEndOfStream() != other.isEndOfStream()) {
+ return isEndOfStream() ? 1 : -1;
+ }
+ long delta = timeUs - other.timeUs;
+ if (delta == 0) {
+ delta = queuedInputBufferCount - other.queuedInputBufferCount;
+ if (delta == 0) {
+ return 0;
+ }
+ }
+ return delta > 0 ? 1 : -1;
+ }
+ }
+
+ private final class CeaOutputBuffer extends SubtitleOutputBuffer {
+
+ @Override
+ public final void release() {
+ releaseOutputBuffer(this);
+ }
+ }
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java
index ddb3804c3e..67271ee218 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/CeaUtil.java
@@ -19,18 +19,19 @@ import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
-/**
- * Utility methods for handling CEA-608/708 messages.
- */
+/** Utility methods for handling CEA-608/708 messages. Defined in A/53 Part 4:2009. */
public final class CeaUtil {
private static final String TAG = "CeaUtil";
private static final int PAYLOAD_TYPE_CC = 4;
private static final int COUNTRY_CODE = 0xB5;
- private static final int PROVIDER_CODE = 0x31;
- private static final int USER_ID = 0x47413934; // "GA94"
+ private static final int PROVIDER_CODE_ATSC = 0x31;
+ private static final int PROVIDER_CODE_DIRECTV = 0x2F;
+ private static final int USER_ID_GA94 = Util.getIntegerCodeForString("GA94");
+ private static final int USER_ID_DTG1 = Util.getIntegerCodeForString("DTG1");
private static final int USER_DATA_TYPE_CODE = 0x3;
/**
@@ -46,33 +47,49 @@ public final class CeaUtil {
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
int payloadType = readNon255TerminatedValue(seiBuffer);
int payloadSize = readNon255TerminatedValue(seiBuffer);
+ int nextPayloadPosition = seiBuffer.getPosition() + payloadSize;
// Process the payload.
if (payloadSize == -1 || payloadSize > seiBuffer.bytesLeft()) {
// This might occur if we're trying to read an encrypted SEI NAL unit.
Log.w(TAG, "Skipping remainder of malformed SEI NAL unit.");
- seiBuffer.setPosition(seiBuffer.limit());
- } else if (isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
- // Ignore country_code (1) + provider_code (2) + user_identifier (4)
- // + user_data_type_code (1).
- seiBuffer.skipBytes(8);
- // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
- int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
- // Ignore em_data (1)
- seiBuffer.skipBytes(1);
- // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
- // + cc_data_1 (8) + cc_data_2 (8).
- int sampleLength = ccCount * 3;
- int sampleStartPosition = seiBuffer.getPosition();
- for (TrackOutput output : outputs) {
- seiBuffer.setPosition(sampleStartPosition);
- output.sampleData(seiBuffer, sampleLength);
- output.sampleMetadata(presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
+ nextPayloadPosition = seiBuffer.limit();
+ } else if (payloadType == PAYLOAD_TYPE_CC && payloadSize >= 8) {
+ int countryCode = seiBuffer.readUnsignedByte();
+ int providerCode = seiBuffer.readUnsignedShort();
+ int userIdentifier = 0;
+ if (providerCode == PROVIDER_CODE_ATSC) {
+ userIdentifier = seiBuffer.readInt();
+ }
+ int userDataTypeCode = seiBuffer.readUnsignedByte();
+ if (providerCode == PROVIDER_CODE_DIRECTV) {
+ seiBuffer.skipBytes(1); // user_data_length.
+ }
+ boolean messageIsSupportedCeaCaption =
+ countryCode == COUNTRY_CODE
+ && (providerCode == PROVIDER_CODE_ATSC || providerCode == PROVIDER_CODE_DIRECTV)
+ && userDataTypeCode == USER_DATA_TYPE_CODE;
+ if (providerCode == PROVIDER_CODE_ATSC) {
+ messageIsSupportedCeaCaption &=
+ userIdentifier == USER_ID_GA94 || userIdentifier == USER_ID_DTG1;
+ }
+ if (messageIsSupportedCeaCaption) {
+ // Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
+ int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
+ // Ignore em_data (1)
+ seiBuffer.skipBytes(1);
+ // Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
+ // + cc_data_1 (8) + cc_data_2 (8).
+ int sampleLength = ccCount * 3;
+ int sampleStartPosition = seiBuffer.getPosition();
+ for (TrackOutput output : outputs) {
+ seiBuffer.setPosition(sampleStartPosition);
+ output.sampleData(seiBuffer, sampleLength);
+ output.sampleMetadata(
+ presentationTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
+ }
}
- // Ignore trailing information in SEI, if any.
- seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
- } else {
- seiBuffer.skipBytes(payloadSize);
}
+ seiBuffer.setPosition(nextPayloadPosition);
}
}
@@ -97,31 +114,6 @@ public final class CeaUtil {
return value;
}
- /**
- * Inspects an sei message to determine whether it contains CEA-608.
- *
- * The position of {@code payload} is left unchanged.
- *
- * @param payloadType The payload type of the message.
- * @param payloadLength The length of the payload.
- * @param payload A {@link ParsableByteArray} containing the payload.
- * @return Whether the sei message contains CEA-608.
- */
- private static boolean isSeiMessageCea608(int payloadType, int payloadLength,
- ParsableByteArray payload) {
- if (payloadType != PAYLOAD_TYPE_CC || payloadLength < 8) {
- return false;
- }
- int startPosition = payload.getPosition();
- int countryCode = payload.readUnsignedByte();
- int providerCode = payload.readUnsignedShort();
- int userIdentifier = payload.readInt();
- int userDataTypeCode = payload.readUnsignedByte();
- payload.setPosition(startPosition);
- return countryCode == COUNTRY_CODE && providerCode == PROVIDER_CODE
- && userIdentifier == USER_ID && userDataTypeCode == USER_DATA_TYPE_CODE;
- }
-
private CeaUtil() {}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
index dbdc0434a1..df5b19c052 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/dvb/DvbDecoder.java
@@ -19,9 +19,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.List;
-/**
- * A {@link SimpleSubtitleDecoder} for DVB Subtitles.
- */
+/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */
public final class DvbDecoder extends SimpleSubtitleDecoder {
private final DvbParser parser;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java
new file mode 100644
index 0000000000..6d60da7d81
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2018 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.text.pgs;
+
+import android.graphics.Bitmap;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
+import com.google.android.exoplayer2.text.Subtitle;
+import com.google.android.exoplayer2.text.SubtitleDecoderException;
+import com.google.android.exoplayer2.util.ParsableByteArray;
+import com.google.android.exoplayer2.util.Util;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+
+/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */
+public final class PgsDecoder extends SimpleSubtitleDecoder {
+
+ private static final int SECTION_TYPE_PALETTE = 0x14;
+ private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15;
+ private static final int SECTION_TYPE_IDENTIFIER = 0x16;
+ private static final int SECTION_TYPE_END = 0x80;
+
+ private final ParsableByteArray buffer;
+ private final CueBuilder cueBuilder;
+
+ public PgsDecoder() {
+ super("PgsDecoder");
+ buffer = new ParsableByteArray();
+ cueBuilder = new CueBuilder();
+ }
+
+ @Override
+ protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException {
+ buffer.reset(data, size);
+ cueBuilder.reset();
+ ArrayList cues = new ArrayList<>();
+ while (buffer.bytesLeft() >= 3) {
+ Cue cue = readNextSection(buffer, cueBuilder);
+ if (cue != null) {
+ cues.add(cue);
+ }
+ }
+ return new PgsSubtitle(Collections.unmodifiableList(cues));
+ }
+
+ private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) {
+ int limit = buffer.limit();
+ int sectionType = buffer.readUnsignedByte();
+ int sectionLength = buffer.readUnsignedShort();
+
+ int nextSectionPosition = buffer.getPosition() + sectionLength;
+ if (nextSectionPosition > limit) {
+ buffer.setPosition(limit);
+ return null;
+ }
+
+ Cue cue = null;
+ switch (sectionType) {
+ case SECTION_TYPE_PALETTE:
+ cueBuilder.parsePaletteSection(buffer, sectionLength);
+ break;
+ case SECTION_TYPE_BITMAP_PICTURE:
+ cueBuilder.parseBitmapSection(buffer, sectionLength);
+ break;
+ case SECTION_TYPE_IDENTIFIER:
+ cueBuilder.parseIdentifierSection(buffer, sectionLength);
+ break;
+ case SECTION_TYPE_END:
+ cue = cueBuilder.build();
+ cueBuilder.reset();
+ break;
+ default:
+ break;
+ }
+
+ buffer.setPosition(nextSectionPosition);
+ return cue;
+ }
+
+ private static final class CueBuilder {
+
+ private final ParsableByteArray bitmapData;
+ private final int[] colors;
+
+ private boolean colorsSet;
+ private int planeWidth;
+ private int planeHeight;
+ private int bitmapX;
+ private int bitmapY;
+ private int bitmapWidth;
+ private int bitmapHeight;
+
+ public CueBuilder() {
+ bitmapData = new ParsableByteArray();
+ colors = new int[256];
+ }
+
+ private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) {
+ if ((sectionLength % 5) != 2) {
+ // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries.
+ return;
+ }
+ buffer.skipBytes(2);
+
+ Arrays.fill(colors, 0);
+ int entryCount = sectionLength / 5;
+ for (int i = 0; i < entryCount; i++) {
+ int index = buffer.readUnsignedByte();
+ int y = buffer.readUnsignedByte();
+ int cr = buffer.readUnsignedByte();
+ int cb = buffer.readUnsignedByte();
+ int a = buffer.readUnsignedByte();
+ int r = (int) (y + (1.40200 * (cr - 128)));
+ int g = (int) (y - (0.34414 * (cb - 128)) - (0.71414 * (cr - 128)));
+ int b = (int) (y + (1.77200 * (cb - 128)));
+ colors[index] =
+ (a << 24)
+ | (Util.constrainValue(r, 0, 255) << 16)
+ | (Util.constrainValue(g, 0, 255) << 8)
+ | Util.constrainValue(b, 0, 255);
+ }
+ colorsSet = true;
+ }
+
+ private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) {
+ if (sectionLength < 4) {
+ return;
+ }
+ buffer.skipBytes(3); // Id (2 bytes), version (1 byte).
+ boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0;
+ sectionLength -= 4;
+
+ if (isBaseSection) {
+ if (sectionLength < 7) {
+ return;
+ }
+ int totalLength = buffer.readUnsignedInt24();
+ if (totalLength < 4) {
+ return;
+ }
+ bitmapWidth = buffer.readUnsignedShort();
+ bitmapHeight = buffer.readUnsignedShort();
+ bitmapData.reset(totalLength - 4);
+ sectionLength -= 7;
+ }
+
+ int position = bitmapData.getPosition();
+ int limit = bitmapData.limit();
+ if (position < limit && sectionLength > 0) {
+ int bytesToRead = Math.min(sectionLength, limit - position);
+ buffer.readBytes(bitmapData.data, position, bytesToRead);
+ bitmapData.setPosition(position + bytesToRead);
+ }
+ }
+
+ private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) {
+ if (sectionLength < 19) {
+ return;
+ }
+ planeWidth = buffer.readUnsignedShort();
+ planeHeight = buffer.readUnsignedShort();
+ buffer.skipBytes(11);
+ bitmapX = buffer.readUnsignedShort();
+ bitmapY = buffer.readUnsignedShort();
+ }
+
+ public Cue build() {
+ if (planeWidth == 0
+ || planeHeight == 0
+ || bitmapWidth == 0
+ || bitmapHeight == 0
+ || bitmapData.limit() == 0
+ || bitmapData.getPosition() != bitmapData.limit()
+ || !colorsSet) {
+ return null;
+ }
+ // Build the bitmapData.
+ bitmapData.setPosition(0);
+ int[] argbBitmapData = new int[bitmapWidth * bitmapHeight];
+ int argbBitmapDataIndex = 0;
+ while (argbBitmapDataIndex < argbBitmapData.length) {
+ int colorIndex = bitmapData.readUnsignedByte();
+ if (colorIndex != 0) {
+ argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex];
+ } else {
+ int switchBits = bitmapData.readUnsignedByte();
+ if (switchBits != 0) {
+ int runLength =
+ (switchBits & 0x40) == 0
+ ? (switchBits & 0x3F)
+ : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte());
+ int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()];
+ Arrays.fill(
+ argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color);
+ argbBitmapDataIndex += runLength;
+ }
+ }
+ }
+ Bitmap bitmap =
+ Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
+ // Build the cue.
+ return new Cue(
+ bitmap,
+ (float) bitmapX / planeWidth,
+ Cue.ANCHOR_TYPE_START,
+ (float) bitmapY / planeHeight,
+ Cue.ANCHOR_TYPE_START,
+ (float) bitmapWidth / planeWidth,
+ (float) bitmapHeight / planeHeight);
+ }
+
+ public void reset() {
+ planeWidth = 0;
+ planeHeight = 0;
+ bitmapX = 0;
+ bitmapY = 0;
+ bitmapWidth = 0;
+ bitmapHeight = 0;
+ bitmapData.reset(0);
+ colorsSet = false;
+ }
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java
new file mode 100644
index 0000000000..9f9af6b6a4
--- /dev/null
+++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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.text.pgs;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.Subtitle;
+import java.util.List;
+
+/** A representation of a PGS subtitle. */
+/* package */ final class PgsSubtitle implements Subtitle {
+
+ private final List cues;
+
+ public PgsSubtitle(List cues) {
+ this.cues = cues;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ return C.INDEX_UNSET;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return 1;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ return 0;
+ }
+
+ @Override
+ public List getCues(long timeUs) {
+ return cues;
+ }
+}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
index f9eddab286..973155c2e3 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java
@@ -15,12 +15,13 @@
*/
package com.google.android.exoplayer2.trackselection;
-import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.MediaChunk;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
+import com.google.android.exoplayer2.util.Clock;
+import com.google.android.exoplayer2.util.Util;
import java.util.List;
/**
@@ -41,17 +42,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final int minDurationToRetainAfterDiscardMs;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
+ private final long minTimeBetweenBufferReevaluationMs;
+ private final Clock clock;
/**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
*/
public Factory(BandwidthMeter bandwidthMeter) {
- this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+ this(
+ bandwidthMeter,
+ DEFAULT_MAX_INITIAL_BITRATE,
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
DEFAULT_BANDWIDTH_FRACTION,
- DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
}
/**
@@ -73,37 +80,55 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
int minDurationToRetainAfterDiscardMs, float bandwidthFraction) {
- this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs,
- maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs,
- bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
+ this(
+ bandwidthMeter,
+ maxInitialBitrate,
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bandwidthFraction,
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
}
/**
* @param bandwidthMeter Provides an estimate of the currently available bandwidth.
- * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
- * when a bandwidth estimate is unavailable.
- * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
- * the selected track to switch to one of higher quality.
- * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
- * the selected track to switch to one of lower quality.
+ * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a
+ * bandwidth estimate is unavailable.
+ * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the
+ * selected track to switch to one of higher quality.
+ * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
+ * selected track to switch to one of lower quality.
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
* quality, the selection may indicate that media already buffered at the lower quality can
* be discarded to speed up the switch. This is the minimum duration of media that must be
* retained at the lower quality.
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
- * consider available for use. Setting to a value less than 1 is recommended to account
- * for inaccuracies in the bandwidth estimator.
- * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of
- * the duration from current playback position to the live edge that has to be buffered
- * before the selected track can be switched to one of higher quality. This parameter is
- * only applied when the playback position is closer to the live edge than
- * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a
- * higher quality from happening.
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
+ * duration from current playback position to the live edge that has to be buffered before
+ * the selected track can be switched to one of higher quality. This parameter is only
+ * applied when the playback position is closer to the live edge than {@code
+ * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
+ * quality from happening.
+ * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
+ * buffer and discard some chunks of lower quality to improve the playback quality if
+ * network conditions have changed. This is the minimum duration between 2 consecutive
+ * buffer reevaluation calls.
+ * @param clock A {@link Clock}.
*/
- public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate,
- int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs,
- int minDurationToRetainAfterDiscardMs, float bandwidthFraction,
- float bufferedFractionToLiveEdgeForQualityIncrease) {
+ public Factory(
+ BandwidthMeter bandwidthMeter,
+ int maxInitialBitrate,
+ int minDurationForQualityIncreaseMs,
+ int maxDurationForQualityDecreaseMs,
+ int minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate;
this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs;
@@ -112,14 +137,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
+ this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
+ this.clock = clock;
}
@Override
public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) {
- return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate,
- minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs,
- minDurationToRetainAfterDiscardMs, bandwidthFraction,
- bufferedFractionToLiveEdgeForQualityIncrease);
+ return new AdaptiveTrackSelection(
+ group,
+ tracks,
+ bandwidthMeter,
+ maxInitialBitrate,
+ minDurationForQualityIncreaseMs,
+ maxDurationForQualityDecreaseMs,
+ minDurationToRetainAfterDiscardMs,
+ bandwidthFraction,
+ bufferedFractionToLiveEdgeForQualityIncrease,
+ minTimeBetweenBufferReevaluationMs,
+ clock);
}
}
@@ -130,6 +165,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f;
+ public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000;
private final BandwidthMeter bandwidthMeter;
private final int maxInitialBitrate;
@@ -138,9 +174,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
private final long minDurationToRetainAfterDiscardUs;
private final float bandwidthFraction;
private final float bufferedFractionToLiveEdgeForQualityIncrease;
+ private final long minTimeBetweenBufferReevaluationMs;
+ private final Clock clock;
+ private float playbackSpeed;
private int selectedIndex;
private int reason;
+ private long lastBufferEvaluationMs;
/**
* @param group The {@link TrackGroup}.
@@ -150,12 +190,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
*/
public AdaptiveTrackSelection(TrackGroup group, int[] tracks,
BandwidthMeter bandwidthMeter) {
- this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+ this(
+ group,
+ tracks,
+ bandwidthMeter,
+ DEFAULT_MAX_INITIAL_BITRATE,
DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
DEFAULT_BANDWIDTH_FRACTION,
- DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE);
+ DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE,
+ DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS,
+ Clock.DEFAULT);
}
/**
@@ -170,23 +216,35 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
* @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the
* selected track to switch to one of lower quality.
* @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher
- * quality, the selection may indicate that media already buffered at the lower quality can
- * be discarded to speed up the switch. This is the minimum duration of media that must be
+ * quality, the selection may indicate that media already buffered at the lower quality can be
+ * discarded to speed up the switch. This is the minimum duration of media that must be
* retained at the lower quality.
* @param bandwidthFraction The fraction of the available bandwidth that the selection should
- * consider available for use. Setting to a value less than 1 is recommended to account
- * for inaccuracies in the bandwidth estimator.
- * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of
- * the duration from current playback position to the live edge that has to be buffered
- * before the selected track can be switched to one of higher quality. This parameter is
- * only applied when the playback position is closer to the live edge than
- * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a
- * higher quality from happening.
+ * consider available for use. Setting to a value less than 1 is recommended to account for
+ * inaccuracies in the bandwidth estimator.
+ * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the
+ * duration from current playback position to the live edge that has to be buffered before the
+ * selected track can be switched to one of higher quality. This parameter is only applied
+ * when the playback position is closer to the live edge than {@code
+ * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher
+ * quality from happening.
+ * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its
+ * buffer and discard some chunks of lower quality to improve the playback quality if network
+ * condition has changed. This is the minimum duration between 2 consecutive buffer
+ * reevaluation calls.
*/
- public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter,
- int maxInitialBitrate, long minDurationForQualityIncreaseMs,
- long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs,
- float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) {
+ public AdaptiveTrackSelection(
+ TrackGroup group,
+ int[] tracks,
+ BandwidthMeter bandwidthMeter,
+ int maxInitialBitrate,
+ long minDurationForQualityIncreaseMs,
+ long maxDurationForQualityDecreaseMs,
+ long minDurationToRetainAfterDiscardMs,
+ float bandwidthFraction,
+ float bufferedFractionToLiveEdgeForQualityIncrease,
+ long minTimeBetweenBufferReevaluationMs,
+ Clock clock) {
super(group, tracks);
this.bandwidthMeter = bandwidthMeter;
this.maxInitialBitrate = maxInitialBitrate;
@@ -196,14 +254,28 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
this.bandwidthFraction = bandwidthFraction;
this.bufferedFractionToLiveEdgeForQualityIncrease =
bufferedFractionToLiveEdgeForQualityIncrease;
+ this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs;
+ this.clock = clock;
+ playbackSpeed = 1f;
selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE);
reason = C.SELECTION_REASON_INITIAL;
+ lastBufferEvaluationMs = C.TIME_UNSET;
+ }
+
+ @Override
+ public void enable() {
+ lastBufferEvaluationMs = C.TIME_UNSET;
+ }
+
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ this.playbackSpeed = playbackSpeed;
}
@Override
public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs,
long availableDurationUs) {
- long nowMs = SystemClock.elapsedRealtime();
+ long nowMs = clock.elapsedRealtime();
// Stash the current selection, then make a new one.
int currentSelectedIndex = selectedIndex;
selectedIndex = determineIdealSelectedIndex(nowMs);
@@ -250,15 +322,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
@Override
public int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue) {
+ long nowMs = clock.elapsedRealtime();
+ if (lastBufferEvaluationMs != C.TIME_UNSET
+ && nowMs - lastBufferEvaluationMs < minTimeBetweenBufferReevaluationMs) {
+ return queue.size();
+ }
+ lastBufferEvaluationMs = nowMs;
if (queue.isEmpty()) {
return 0;
}
+
int queueSize = queue.size();
- long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs;
- if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) {
+ MediaChunk lastChunk = queue.get(queueSize - 1);
+ long playoutBufferedDurationBeforeLastChunkUs =
+ Util.getPlayoutDurationForMediaDuration(
+ lastChunk.startTimeUs - playbackPositionUs, playbackSpeed);
+ if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) {
return queueSize;
}
- int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime());
+ int idealSelectedIndex = determineIdealSelectedIndex(nowMs);
Format idealFormat = getFormat(idealSelectedIndex);
// If the chunks contain video, discard from the first SD chunk beyond
// minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal
@@ -266,8 +348,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
for (int i = 0; i < queueSize; i++) {
MediaChunk chunk = queue.get(i);
Format format = chunk.trackFormat;
- long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
- if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
+ long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs;
+ long playoutDurationBeforeThisChunkUs =
+ Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed);
+ if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs
&& format.bitrate < idealFormat.bitrate
&& format.height != Format.NO_VALUE && format.height < 720
&& format.width != Format.NO_VALUE && format.width < 1280
@@ -281,8 +365,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
/**
* Computes the ideal selected index ignoring buffer health.
*
- * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or
- * {@link Long#MIN_VALUE} to ignore blacklisting.
+ * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link
+ * Long#MIN_VALUE} to ignore blacklisting.
*/
private int determineIdealSelectedIndex(long nowMs) {
long bitrateEstimate = bandwidthMeter.getBitrateEstimate();
@@ -292,7 +376,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection {
for (int i = 0; i < length; i++) {
if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) {
Format format = getFormat(i);
- if (format.bitrate <= effectiveBitrate) {
+ if (Math.round(format.bitrate * playbackSpeed) <= effectiveBitrate) {
return i;
} else {
lowestBitrateNonBlacklistedIndex = i;
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
index 6bc6afb88b..9a58ac07aa 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java
@@ -138,6 +138,11 @@ public abstract class BaseTrackSelection implements TrackSelection {
return tracks[getSelectedIndex()];
}
+ @Override
+ public void onPlaybackSpeed(float playbackSpeed) {
+ // Do nothing.
+ }
+
@Override
public int evaluateQueueSize(long playbackPositionUs, List extends MediaChunk> queue) {
return queue.size();
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
index 49b8e8964b..509e86345e 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection;
import android.content.Context;
import android.graphics.Point;
+import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@@ -74,11 +75,278 @@ import java.util.concurrent.atomic.AtomicReference;
*/
public class DefaultTrackSelector extends MappingTrackSelector {
+ /**
+ * A builder for {@link Parameters}.
+ */
+ public static final class ParametersBuilder {
+
+ private String preferredAudioLanguage;
+ private String preferredTextLanguage;
+ private boolean selectUndeterminedTextLanguage;
+ private int disabledTextTrackSelectionFlags;
+ private boolean forceLowestBitrate;
+ private boolean allowMixedMimeAdaptiveness;
+ private boolean allowNonSeamlessAdaptiveness;
+ private int maxVideoWidth;
+ private int maxVideoHeight;
+ private int maxVideoBitrate;
+ private boolean exceedVideoConstraintsIfNecessary;
+ private boolean exceedRendererCapabilitiesIfNecessary;
+ private int viewportWidth;
+ private int viewportHeight;
+ private boolean viewportOrientationMayChange;
+
+ /**
+ * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}.
+ */
+ public ParametersBuilder() {
+ this(Parameters.DEFAULT);
+ }
+
+ /**
+ * @param initialValues The {@link Parameters} from which the initial values of the builder are
+ * obtained.
+ */
+ private ParametersBuilder(Parameters initialValues) {
+ preferredAudioLanguage = initialValues.preferredAudioLanguage;
+ preferredTextLanguage = initialValues.preferredTextLanguage;
+ selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage;
+ disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags;
+ forceLowestBitrate = initialValues.forceLowestBitrate;
+ allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness;
+ allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness;
+ maxVideoWidth = initialValues.maxVideoWidth;
+ maxVideoHeight = initialValues.maxVideoHeight;
+ maxVideoBitrate = initialValues.maxVideoBitrate;
+ exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary;
+ exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary;
+ viewportWidth = initialValues.viewportWidth;
+ viewportHeight = initialValues.viewportHeight;
+ viewportOrientationMayChange = initialValues.viewportOrientationMayChange;
+ }
+
+ /**
+ * See {@link Parameters#preferredAudioLanguage}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) {
+ this.preferredAudioLanguage = preferredAudioLanguage;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#preferredTextLanguage}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) {
+ this.preferredTextLanguage = preferredTextLanguage;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#selectUndeterminedTextLanguage}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setSelectUndeterminedTextLanguage(
+ boolean selectUndeterminedTextLanguage) {
+ this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#disabledTextTrackSelectionFlags}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setDisabledTextTrackSelectionFlags(
+ int disabledTextTrackSelectionFlags) {
+ this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#forceLowestBitrate}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) {
+ this.forceLowestBitrate = forceLowestBitrate;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#allowMixedMimeAdaptiveness}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) {
+ this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#allowNonSeamlessAdaptiveness}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) {
+ this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness;
+ return this;
+ }
+
+ /**
+ * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoSizeSd() {
+ return setMaxVideoSize(1279, 719);
+ }
+
+ /**
+ * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder clearVideoSizeConstraints() {
+ return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
+ }
+
+ /**
+ * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
+ this.maxVideoWidth = maxVideoWidth;
+ this.maxVideoHeight = maxVideoHeight;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#maxVideoBitrate}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) {
+ this.maxVideoBitrate = maxVideoBitrate;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#exceedVideoConstraintsIfNecessary}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setExceedVideoConstraintsIfNecessary(
+ boolean exceedVideoConstraintsIfNecessary) {
+ this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary;
+ return this;
+ }
+
+ /**
+ * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setExceedRendererCapabilitiesIfNecessary(
+ boolean exceedRendererCapabilitiesIfNecessary) {
+ this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary;
+ return this;
+ }
+
+ /**
+ * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from
+ * {@link Util#getPhysicalDisplaySize(Context)}.
+ *
+ * @param context The context to obtain the viewport size from.
+ * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}.
+ * @return This builder.
+ */
+ public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context,
+ boolean viewportOrientationMayChange) {
+ // Assume the viewport is fullscreen.
+ Point viewportSize = Util.getPhysicalDisplaySize(context);
+ return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange);
+ }
+
+ /**
+ * Equivalent to
+ * {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder clearViewportSizeConstraints() {
+ return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
+ }
+
+ /**
+ * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and
+ * {@link Parameters#viewportOrientationMayChange}.
+ *
+ * @return This builder.
+ */
+ public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight,
+ boolean viewportOrientationMayChange) {
+ this.viewportWidth = viewportWidth;
+ this.viewportHeight = viewportHeight;
+ this.viewportOrientationMayChange = viewportOrientationMayChange;
+ return this;
+ }
+
+ /**
+ * Builds a {@link Parameters} instance with the selected values.
+ */
+ public Parameters build() {
+ return new Parameters(
+ preferredAudioLanguage,
+ preferredTextLanguage,
+ selectUndeterminedTextLanguage,
+ disabledTextTrackSelectionFlags,
+ forceLowestBitrate,
+ allowMixedMimeAdaptiveness,
+ allowNonSeamlessAdaptiveness,
+ maxVideoWidth,
+ maxVideoHeight,
+ maxVideoBitrate,
+ exceedVideoConstraintsIfNecessary,
+ exceedRendererCapabilitiesIfNecessary,
+ viewportWidth,
+ viewportHeight,
+ viewportOrientationMayChange);
+ }
+
+ }
+
/**
* Constraint parameters for {@link DefaultTrackSelector}.
*/
public static final class Parameters {
+ /**
+ * An instance with default values:
+ *
+ *