From a60ed20f1bfac6d750da9383a03834b35ce9ab13 Mon Sep 17 00:00:00 2001 From: gyumin Date: Tue, 9 Mar 2021 07:31:48 +0000 Subject: [PATCH] Implement Bundleable for Timeline.Window PiperOrigin-RevId: 361741354 --- .../google/android/exoplayer2/MediaItem.java | 91 ++++++----- .../google/android/exoplayer2/Timeline.java | 154 +++++++++++++++++- .../android/exoplayer2/TimelineTest.java | 33 ++++ 3 files changed, 229 insertions(+), 49 deletions(-) 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 { *

Information defined by a
    * timeline window */ - 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();