diff --git a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
index ebc41366b2..b9b7922f7e 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/MediaItem.java
@@ -1272,6 +1272,12 @@ public final class MediaItem implements Bundleable {
private static final int FIELD_MEDIA_METADATA = 2;
private static final int FIELD_CLIPPING_PROPERTIES = 3;
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #playbackProperties} field. The {@link #playbackProperties} of an
+ * instance restored by {@link #CREATOR} will always be {@code null}.
+ */
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
@@ -1282,46 +1288,51 @@ public final class MediaItem implements Bundleable {
return bundle;
}
- /** Object that can restore {@link MediaItem} from a {@link Bundle}. */
- public static final Creator CREATOR =
- bundle -> {
- String mediaId = checkNotNull(bundle.getString(keyForField(FIELD_MEDIA_ID)));
- @Nullable
- Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
- LiveConfiguration liveConfiguration;
- if (liveConfigurationBundle == null) {
- liveConfiguration = LiveConfiguration.UNSET;
- } else {
- liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle);
- }
- @Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA));
- MediaMetadata mediaMetadata;
- if (mediaMetadataBundle == null) {
- mediaMetadata = new MediaMetadata.Builder().build();
- } else {
- mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle);
- }
- @Nullable
- Bundle clippingPropertiesBundle = bundle.getBundle(keyForField(FIELD_CLIPPING_PROPERTIES));
- ClippingProperties clippingProperties;
- if (clippingPropertiesBundle == null) {
- clippingProperties =
- new ClippingProperties(
- /* startPositionMs= */ 0,
- /* endPositionMs= */ C.TIME_END_OF_SOURCE,
- /* relativeToLiveWindow= */ false,
- /* relativeToDefaultPosition= */ false,
- /* startsAtKeyFrame= */ false);
- } else {
- clippingProperties = ClippingProperties.CREATOR.fromBundle(clippingPropertiesBundle);
- }
- return new MediaItem(
- mediaId,
- clippingProperties,
- /* playbackProperties= */ null,
- liveConfiguration,
- mediaMetadata);
- };
+ /**
+ * Object that can restore {@link MediaItem} from a {@link Bundle}.
+ *
+ * The {@link #playbackProperties} of a restored instance will always be {@code null}.
+ */
+ public static final Creator CREATOR = MediaItem::fromBundle;
+
+ private static MediaItem fromBundle(Bundle bundle) {
+ String mediaId = checkNotNull(bundle.getString(keyForField(FIELD_MEDIA_ID)));
+ @Nullable
+ Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
+ LiveConfiguration liveConfiguration;
+ if (liveConfigurationBundle == null) {
+ liveConfiguration = LiveConfiguration.UNSET;
+ } else {
+ liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle);
+ }
+ @Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA));
+ MediaMetadata mediaMetadata;
+ if (mediaMetadataBundle == null) {
+ mediaMetadata = new MediaMetadata.Builder().build();
+ } else {
+ mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle);
+ }
+ @Nullable
+ Bundle clippingPropertiesBundle = bundle.getBundle(keyForField(FIELD_CLIPPING_PROPERTIES));
+ ClippingProperties clippingProperties;
+ if (clippingPropertiesBundle == null) {
+ clippingProperties =
+ new ClippingProperties(
+ /* startPositionMs= */ 0,
+ /* endPositionMs= */ C.TIME_END_OF_SOURCE,
+ /* relativeToLiveWindow= */ false,
+ /* relativeToDefaultPosition= */ false,
+ /* startsAtKeyFrame= */ false);
+ } else {
+ clippingProperties = ClippingProperties.CREATOR.fromBundle(clippingPropertiesBundle);
+ }
+ return new MediaItem(
+ mediaId,
+ clippingProperties,
+ /* playbackProperties= */ null,
+ liveConfiguration,
+ mediaMetadata);
+ }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
diff --git a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
index 42bc5897c1..5b7a4fbca5 100644
--- a/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
+++ b/library/common/src/main/java/com/google/android/exoplayer2/Timeline.java
@@ -136,13 +136,15 @@ public abstract class Timeline {
*
*/
- public static final class Window {
+ public static final class Window implements Bundleable {
/**
* A {@link #uid} for a window that must be used for single-window {@link Timeline Timelines}.
*/
public static final Object SINGLE_WINDOW_UID = new Object();
+ private static final Object FAKE_WINDOW_UID = new Object();
+
private static final MediaItem EMPTY_MEDIA_ITEM =
new MediaItem.Builder()
.setMediaId("com.google.android.exoplayer2.Timeline")
@@ -213,14 +215,6 @@ public abstract class Timeline {
*/
public boolean isPlaceholder;
- /** The index of the first period that belongs to this window. */
- public int firstPeriodIndex;
-
- /**
- * The index of the last period that belongs to this window.
- */
- public int lastPeriodIndex;
-
/**
* The default position relative to the start of the window at which to begin playback, in
* microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
@@ -234,6 +228,12 @@ public abstract class Timeline {
*/
public long durationUs;
+ /** The index of the first period that belongs to this window. */
+ public int firstPeriodIndex;
+
+ /** The index of the last period that belongs to this window. */
+ public int lastPeriodIndex;
+
/**
* The position of the start of this window relative to the start of the first period belonging
* to it, in microseconds.
@@ -404,6 +404,142 @@ public abstract class Timeline {
result = 31 * result + (int) (positionInFirstPeriodUs ^ (positionInFirstPeriodUs >>> 32));
return result;
}
+
+ // Bundleable implementation.
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ FIELD_MEDIA_ITEM,
+ FIELD_PRESENTATION_START_TIME_MS,
+ FIELD_WINDOW_START_TIME_MS,
+ FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS,
+ FIELD_IS_SEEKABLE,
+ FIELD_IS_DYNAMIC,
+ FIELD_LIVE_CONFIGURATION,
+ FIELD_IS_PLACEHOLDER,
+ FIELD_DEFAULT_POSITION_US,
+ FIELD_DURATION_US,
+ FIELD_FIRST_PERIOD_INDEX,
+ FIELD_LAST_PERIOD_INDEX,
+ FIELD_POSITION_IN_FIRST_PERIOD_US,
+ })
+ private @interface FieldNumber {}
+
+ private static final int FIELD_MEDIA_ITEM = 1;
+ private static final int FIELD_PRESENTATION_START_TIME_MS = 2;
+ private static final int FIELD_WINDOW_START_TIME_MS = 3;
+ private static final int FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS = 4;
+ private static final int FIELD_IS_SEEKABLE = 5;
+ private static final int FIELD_IS_DYNAMIC = 6;
+ private static final int FIELD_LIVE_CONFIGURATION = 7;
+ private static final int FIELD_IS_PLACEHOLDER = 8;
+ private static final int FIELD_DEFAULT_POSITION_US = 9;
+ private static final int FIELD_DURATION_US = 10;
+ private static final int FIELD_FIRST_PERIOD_INDEX = 11;
+ private static final int FIELD_LAST_PERIOD_INDEX = 12;
+ private static final int FIELD_POSITION_IN_FIRST_PERIOD_US = 13;
+
+ /**
+ * {@inheritDoc}
+ *
+ *
It omits the {@link #uid} and {@link #manifest} fields. The {@link #uid} of an instance
+ * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the
+ * instance will be {@code null}.
+ */
+ // TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise.
+ @Override
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle());
+ bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs);
+ bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs);
+ bundle.putLong(
+ keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs);
+ bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable);
+ bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic);
+ @Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration;
+ if (liveConfiguration != null) {
+ bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
+ }
+ bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder);
+ bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs);
+ bundle.putLong(keyForField(FIELD_DURATION_US), durationUs);
+ bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex);
+ bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex);
+ bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs);
+ return bundle;
+ }
+
+ /**
+ * Object that can restore {@link Period} from a {@link Bundle}.
+ *
+ *
The {@link #uid} of a restored instance will be a fake {@link Object} and the {@link
+ * #manifest} of the instance will be {@code null}.
+ */
+ public static final Creator CREATOR = Window::fromBundle;
+
+ private static Window fromBundle(Bundle bundle) {
+ @Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM));
+ @Nullable
+ MediaItem mediaItem =
+ mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : null;
+ long presentationStartTimeMs =
+ bundle.getLong(
+ keyForField(FIELD_PRESENTATION_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET);
+ long windowStartTimeMs =
+ bundle.getLong(keyForField(FIELD_WINDOW_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET);
+ long elapsedRealtimeEpochOffsetMs =
+ bundle.getLong(
+ keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS),
+ /* defaultValue= */ C.TIME_UNSET);
+ boolean isSeekable =
+ bundle.getBoolean(keyForField(FIELD_IS_SEEKABLE), /* defaultValue= */ false);
+ boolean isDynamic =
+ bundle.getBoolean(keyForField(FIELD_IS_DYNAMIC), /* defaultValue= */ false);
+ @Nullable
+ Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
+ @Nullable
+ MediaItem.LiveConfiguration liveConfiguration =
+ liveConfigurationBundle != null
+ ? MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle)
+ : null;
+ boolean isPlaceHolder =
+ bundle.getBoolean(keyForField(FIELD_IS_PLACEHOLDER), /* defaultValue= */ false);
+ long defaultPositionUs =
+ bundle.getLong(keyForField(FIELD_DEFAULT_POSITION_US), /* defaultValue= */ 0);
+ long durationUs =
+ bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
+ int firstPeriodIndex =
+ bundle.getInt(keyForField(FIELD_FIRST_PERIOD_INDEX), /* defaultValue= */ 0);
+ int lastPeriodIndex =
+ bundle.getInt(keyForField(FIELD_LAST_PERIOD_INDEX), /* defaultValue= */ 0);
+ long positionInFirstPeriodUs =
+ bundle.getLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), /* defaultValue= */ 0);
+
+ Window window = new Window();
+ window.set(
+ FAKE_WINDOW_UID,
+ mediaItem,
+ /* manifest= */ null,
+ presentationStartTimeMs,
+ windowStartTimeMs,
+ elapsedRealtimeEpochOffsetMs,
+ isSeekable,
+ isDynamic,
+ liveConfiguration,
+ defaultPositionUs,
+ durationUs,
+ firstPeriodIndex,
+ lastPeriodIndex,
+ positionInFirstPeriodUs);
+ window.isPlaceholder = isPlaceHolder;
+ return window;
+ }
+
+ private static String keyForField(@Window.FieldNumber int field) {
+ return Integer.toString(field, Character.MAX_RADIX);
+ }
}
/**
diff --git a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java
index 82b91ddfd9..45fde51eda 100644
--- a/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java
+++ b/library/common/src/test/java/com/google/android/exoplayer2/TimelineTest.java
@@ -201,6 +201,39 @@ public class TimelineTest {
assertThat(period.hashCode()).isEqualTo(otherPeriod.hashCode());
}
+ @Test
+ public void roundtripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() {
+ Timeline.Window window = new Timeline.Window();
+ window.uid = new Object();
+ window.mediaItem = new MediaItem.Builder().setMediaId("mediaId").build();
+ window.manifest = new Object();
+ window.presentationStartTimeMs = 111;
+ window.windowStartTimeMs = 222;
+ window.elapsedRealtimeEpochOffsetMs = 333;
+ window.isSeekable = true;
+ window.isDynamic = true;
+ window.liveConfiguration =
+ new LiveConfiguration(
+ /* targetOffsetMs= */ 1,
+ /* minOffsetMs= */ 2,
+ /* maxOffsetMs= */ 3,
+ /* minPlaybackSpeed= */ 0.5f,
+ /* maxPlaybackSpeed= */ 1.5f);
+ window.isPlaceholder = true;
+ window.defaultPositionUs = 444;
+ window.durationUs = 555;
+ window.firstPeriodIndex = 6;
+ window.lastPeriodIndex = 7;
+ window.positionInFirstPeriodUs = 888;
+
+ Timeline.Window restoredWindow = Timeline.Window.CREATOR.fromBundle(window.toBundle());
+
+ assertThat(restoredWindow.manifest).isNull();
+ window.uid = restoredWindow.uid;
+ window.manifest = null;
+ assertThat(restoredWindow).isEqualTo(window);
+ }
+
@Test
public void roundtripViaBundle_ofPeriod_yieldsEqualInstanceExceptIds() {
Timeline.Period period = new Timeline.Period();