| /* |
| * 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 static com.google.android.exoplayer2.source.ads.AdPlaybackState.AD_STATE_UNAVAILABLE; |
| import static com.google.android.exoplayer2.util.Assertions.checkArgument; |
| import static com.google.android.exoplayer2.util.Assertions.checkState; |
| import static java.lang.Math.max; |
| import static java.lang.Math.min; |
| |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.SystemClock; |
| import android.util.Pair; |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.source.ads.AdPlaybackState; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.BundleUtil; |
| import com.google.android.exoplayer2.util.Util; |
| import com.google.common.collect.ImmutableList; |
| import com.google.errorprone.annotations.InlineMe; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * A flexible representation of the structure of media. A timeline is able to represent the |
| * structure of a wide variety of media, from simple cases like a single media file through to |
| * complex compositions of media such as playlists and streams with inserted ads. Instances are |
| * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides |
| * a snapshot of the current state. |
| * |
| * <p>A timeline consists of {@link Window Windows} and {@link Period Periods}. |
| * |
| * <ul> |
| * <li>A {@link Window} usually corresponds to one playlist item. It may span one or more periods |
| * and it defines the region within those periods that's currently available for playback. The |
| * window also provides additional information such as whether seeking is supported within the |
| * window and the default position, which is the position from which playback will start when |
| * the player starts playing the window. |
| * <li>A {@link Period} defines a single logical piece of media, for example a media file. It may |
| * also define groups of ads inserted into the media, along with information about whether |
| * those ads have been loaded and played. |
| * </ul> |
| * |
| * <p>The following examples illustrate timelines for various use cases. |
| * |
| * <h2 id="single-file">Single media file or on-demand stream</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-single-file.svg" alt="Example timeline for a |
| * single file"> |
| * |
| * <p>A timeline for a single media file or on-demand stream consists of a single period and window. |
| * The window spans the whole period, indicating that all parts of the media are available for |
| * playback. The window's default position is typically at the start of the period (indicated by the |
| * black dot in the figure above). |
| * |
| * <h2>Playlist of media files or on-demand streams</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-playlist.svg" alt="Example timeline for a |
| * playlist of files"> |
| * |
| * <p>A timeline for a playlist of media files or on-demand streams consists of multiple periods, |
| * each with its own window. Each window spans the whole of the corresponding period, and typically |
| * has a default position at the start of the period. The properties of the periods and windows |
| * (e.g. their durations and whether the window is seekable) will often only become known when the |
| * player starts buffering the corresponding file or stream. |
| * |
| * <h2 id="live-limited">Live stream with limited availability</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-live-limited.svg" alt="Example timeline for |
| * a live stream with limited availability"> |
| * |
| * <p>A timeline for a live stream consists of a period whose duration is unknown, since it's |
| * continually extending as more content is broadcast. If content only remains available for a |
| * limited period of time then the window may start at a non-zero position, defining the region of |
| * content that can still be played. The window will return true from {@link Window#isLive()} to |
| * indicate it's a live stream and {@link Window#isDynamic} will be set to true as long as we expect |
| * changes to the live window. Its default position is typically near to the live edge (indicated by |
| * the black dot in the figure above). |
| * |
| * <h2>Live stream with indefinite availability</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-live-indefinite.svg" alt="Example timeline |
| * for a live stream with indefinite availability"> |
| * |
| * <p>A timeline for a live stream with indefinite availability is similar to the <a |
| * href="#live-limited">Live stream with limited availability</a> case, except that the window |
| * starts at the beginning of the period to indicate that all of the previously broadcast content |
| * can still be played. |
| * |
| * <h2 id="live-multi-period">Live stream with multiple periods</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-live-multi-period.svg" alt="Example timeline |
| * for a live stream with multiple periods"> |
| * |
| * <p>This case arises when a live stream is explicitly divided into separate periods, for example |
| * at content boundaries. This case is similar to the <a href="#live-limited">Live stream with |
| * limited availability</a> case, except that the window may span more than one period. Multiple |
| * periods are also possible in the indefinite availability case. |
| * |
| * <h2>On-demand stream followed by live stream</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-advanced.svg" alt="Example timeline for an |
| * on-demand stream followed by a live stream"> |
| * |
| * <p>This case is the concatenation of the <a href="#single-file">Single media file or on-demand |
| * stream</a> and <a href="#multi-period">Live stream with multiple periods</a> cases. When playback |
| * of the on-demand stream ends, playback of the live stream will start from its default position |
| * near the live edge. |
| * |
| * <h2 id="single-file-midrolls">On-demand stream with mid-roll ads</h2> |
| * |
| * <p style="align:center"><img src="doc-files/timeline-single-file-midrolls.svg" alt="Example |
| * timeline for an on-demand stream with mid-roll ad groups"> |
| * |
| * <p>This case includes mid-roll ad groups, which are defined as part of the timeline's single |
| * period. The period can be queried for information about the ad groups and the ads they contain. |
| */ |
| public abstract class Timeline implements Bundleable { |
| |
| /** |
| * Holds information about a window in a {@link Timeline}. A window usually corresponds to one |
| * playlist item and defines a region of media currently available for playback along with |
| * additional information such as whether seeking is supported within the window. The figure below |
| * shows some of the information defined by a window, as well as how this information relates to |
| * corresponding {@link Period Periods} in the timeline. |
| * |
| * <p style="align:center"><img src="doc-files/timeline-window.svg" alt="Information defined by a |
| * timeline 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") |
| .setUri(Uri.EMPTY) |
| .build(); |
| |
| /** |
| * A unique identifier for the window. Single-window {@link Timeline Timelines} must use {@link |
| * #SINGLE_WINDOW_UID}. |
| */ |
| public Object uid; |
| |
| /** @deprecated Use {@link #mediaItem} instead. */ |
| @Deprecated @Nullable public Object tag; |
| |
| /** The {@link MediaItem} associated to the window. Not necessarily unique. */ |
| public MediaItem mediaItem; |
| |
| /** The manifest of the window. May be {@code null}. */ |
| @Nullable public Object manifest; |
| |
| /** |
| * The start time of the presentation to which this window belongs in milliseconds since the |
| * Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes |
| * only. |
| */ |
| public long presentationStartTimeMs; |
| |
| /** |
| * The window's start time in milliseconds since the Unix epoch, or {@link C#TIME_UNSET} if |
| * unknown or not applicable. |
| */ |
| public long windowStartTimeMs; |
| |
| /** |
| * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch |
| * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not |
| * applicable. |
| * |
| * <p>Note that the current Unix time can be retrieved using {@link #getCurrentUnixTimeMs()} and |
| * is calculated as {@code SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs}. |
| */ |
| public long elapsedRealtimeEpochOffsetMs; |
| |
| /** Whether it's possible to seek within this window. */ |
| public boolean isSeekable; |
| |
| // TODO: Split this to better describe which parts of the window might change. For example it |
| // should be possible to individually determine whether the start and end positions of the |
| // window may change relative to the underlying periods. For an example of where it's useful to |
| // know that the end position is fixed whilst the start position may still change, see: |
| // https://github.com/google/ExoPlayer/issues/4780. |
| /** Whether this window may change when the timeline is updated. */ |
| public boolean isDynamic; |
| |
| /** @deprecated Use {@link #isLive()} instead. */ |
| @Deprecated public boolean isLive; |
| |
| /** |
| * The {@link MediaItem.LiveConfiguration} that is used or null if {@link #isLive()} returns |
| * false. |
| */ |
| @Nullable public MediaItem.LiveConfiguration liveConfiguration; |
| |
| /** |
| * Whether this window contains placeholder information because the real information has yet to |
| * be loaded. |
| */ |
| public boolean isPlaceholder; |
| |
| /** |
| * 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 |
| * non-zero default position projection, and if the specified projection cannot be performed |
| * whilst remaining within the bounds of the window. |
| */ |
| public long defaultPositionUs; |
| |
| /** The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. */ |
| 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. |
| */ |
| public long positionInFirstPeriodUs; |
| |
| /** Creates window. */ |
| public Window() { |
| uid = SINGLE_WINDOW_UID; |
| mediaItem = EMPTY_MEDIA_ITEM; |
| } |
| |
| /** Sets the data held by this window. */ |
| @SuppressWarnings("deprecation") |
| public Window set( |
| Object uid, |
| @Nullable MediaItem mediaItem, |
| @Nullable Object manifest, |
| long presentationStartTimeMs, |
| long windowStartTimeMs, |
| long elapsedRealtimeEpochOffsetMs, |
| boolean isSeekable, |
| boolean isDynamic, |
| @Nullable MediaItem.LiveConfiguration liveConfiguration, |
| long defaultPositionUs, |
| long durationUs, |
| int firstPeriodIndex, |
| int lastPeriodIndex, |
| long positionInFirstPeriodUs) { |
| this.uid = uid; |
| this.mediaItem = mediaItem != null ? mediaItem : EMPTY_MEDIA_ITEM; |
| this.tag = |
| mediaItem != null && mediaItem.localConfiguration != null |
| ? mediaItem.localConfiguration.tag |
| : null; |
| this.manifest = manifest; |
| this.presentationStartTimeMs = presentationStartTimeMs; |
| this.windowStartTimeMs = windowStartTimeMs; |
| this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; |
| this.isSeekable = isSeekable; |
| this.isDynamic = isDynamic; |
| this.isLive = liveConfiguration != null; |
| this.liveConfiguration = liveConfiguration; |
| this.defaultPositionUs = defaultPositionUs; |
| this.durationUs = durationUs; |
| this.firstPeriodIndex = firstPeriodIndex; |
| this.lastPeriodIndex = lastPeriodIndex; |
| this.positionInFirstPeriodUs = positionInFirstPeriodUs; |
| this.isPlaceholder = false; |
| return this; |
| } |
| |
| /** |
| * Returns the default position relative to the start of the window at which to begin playback, |
| * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a |
| * non-zero default position projection, and if the specified projection cannot be performed |
| * whilst remaining within the bounds of the window. |
| */ |
| public long getDefaultPositionMs() { |
| return Util.usToMs(defaultPositionUs); |
| } |
| |
| /** |
| * Returns 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 |
| * non-zero default position projection, and if the specified projection cannot be performed |
| * whilst remaining within the bounds of the window. |
| */ |
| public long getDefaultPositionUs() { |
| return defaultPositionUs; |
| } |
| |
| /** Returns the duration of the window in milliseconds, or {@link C#TIME_UNSET} if unknown. */ |
| public long getDurationMs() { |
| return Util.usToMs(durationUs); |
| } |
| |
| /** Returns the duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown. */ |
| public long getDurationUs() { |
| return durationUs; |
| } |
| |
| /** |
| * Returns the position of the start of this window relative to the start of the first period |
| * belonging to it, in milliseconds. |
| */ |
| public long getPositionInFirstPeriodMs() { |
| return Util.usToMs(positionInFirstPeriodUs); |
| } |
| |
| /** |
| * Returns the position of the start of this window relative to the start of the first period |
| * belonging to it, in microseconds. |
| */ |
| public long getPositionInFirstPeriodUs() { |
| return positionInFirstPeriodUs; |
| } |
| |
| /** |
| * Returns the current time in milliseconds since the Unix epoch. |
| * |
| * <p>This method applies {@link #elapsedRealtimeEpochOffsetMs known corrections} made available |
| * by the media such that this time corresponds to the clock of the media origin server. |
| */ |
| public long getCurrentUnixTimeMs() { |
| return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs); |
| } |
| |
| /** Returns whether this is a live stream. */ |
| // Verifies whether the deprecated isLive member field is in a correct state. |
| @SuppressWarnings("deprecation") |
| public boolean isLive() { |
| checkState(isLive == (liveConfiguration != null)); |
| return liveConfiguration != null; |
| } |
| |
| // Provide backward compatibility for tag. |
| @Override |
| public boolean equals(@Nullable Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null || !getClass().equals(obj.getClass())) { |
| return false; |
| } |
| Window that = (Window) obj; |
| return Util.areEqual(uid, that.uid) |
| && Util.areEqual(mediaItem, that.mediaItem) |
| && Util.areEqual(manifest, that.manifest) |
| && Util.areEqual(liveConfiguration, that.liveConfiguration) |
| && presentationStartTimeMs == that.presentationStartTimeMs |
| && windowStartTimeMs == that.windowStartTimeMs |
| && elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs |
| && isSeekable == that.isSeekable |
| && isDynamic == that.isDynamic |
| && isPlaceholder == that.isPlaceholder |
| && defaultPositionUs == that.defaultPositionUs |
| && durationUs == that.durationUs |
| && firstPeriodIndex == that.firstPeriodIndex |
| && lastPeriodIndex == that.lastPeriodIndex |
| && positionInFirstPeriodUs == that.positionInFirstPeriodUs; |
| } |
| |
| // Provide backward compatibility for tag. |
| @Override |
| public int hashCode() { |
| int result = 7; |
| result = 31 * result + uid.hashCode(); |
| result = 31 * result + mediaItem.hashCode(); |
| result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); |
| result = 31 * result + (liveConfiguration == null ? 0 : liveConfiguration.hashCode()); |
| result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); |
| result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); |
| result = |
| 31 * result |
| + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); |
| result = 31 * result + (isSeekable ? 1 : 0); |
| result = 31 * result + (isDynamic ? 1 : 0); |
| result = 31 * result + (isPlaceholder ? 1 : 0); |
| result = 31 * result + (int) (defaultPositionUs ^ (defaultPositionUs >>> 32)); |
| result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); |
| result = 31 * result + firstPeriodIndex; |
| result = 31 * result + lastPeriodIndex; |
| 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; |
| |
| private final Bundle toBundle(boolean excludeMediaItem) { |
| Bundle bundle = new Bundle(); |
| bundle.putBundle( |
| keyForField(FIELD_MEDIA_ITEM), |
| excludeMediaItem ? MediaItem.EMPTY.toBundle() : 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; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>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() { |
| return toBundle(/* excludeMediaItem= */ false); |
| } |
| |
| /** |
| * Object that can restore {@link Period} from a {@link Bundle}. |
| * |
| * <p>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<Window> 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); |
| } |
| } |
| |
| /** |
| * Holds information about a period in a {@link Timeline}. A period defines a single logical piece |
| * of media, for example a media file. It may also define groups of ads inserted into the media, |
| * along with information about whether those ads have been loaded and played. |
| * |
| * <p>The figure below shows some of the information defined by a period, as well as how this |
| * information relates to a corresponding {@link Window} in the timeline. |
| * |
| * <p style="align:center"><img src="doc-files/timeline-period.svg" alt="Information defined by a |
| * period"> |
| */ |
| public static final class Period implements Bundleable { |
| |
| /** |
| * An identifier for the period. Not necessarily unique. May be null if the ids of the period |
| * are not required. |
| */ |
| @Nullable public Object id; |
| |
| /** |
| * A unique identifier for the period. May be null if the ids of the period are not required. |
| */ |
| @Nullable public Object uid; |
| |
| /** The index of the window to which this period belongs. */ |
| public int windowIndex; |
| |
| /** The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. */ |
| public long durationUs; |
| |
| /** |
| * The position of the start of this period relative to the start of the window to which it |
| * belongs, in microseconds. May be negative if the start of the period is not within the |
| * window. |
| */ |
| public long positionInWindowUs; |
| |
| /** |
| * Whether this period contains placeholder information because the real information has yet to |
| * be loaded. |
| */ |
| public boolean isPlaceholder; |
| |
| private AdPlaybackState adPlaybackState; |
| |
| /** Creates a new instance with no ad playback state. */ |
| public Period() { |
| adPlaybackState = AdPlaybackState.NONE; |
| } |
| |
| /** |
| * Sets the data held by this period. |
| * |
| * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the |
| * period are not required. |
| * @param uid A unique identifier for the period. May be null if the ids of the period are not |
| * required. |
| * @param windowIndex The index of the window to which this period belongs. |
| * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if |
| * unknown. |
| * @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. |
| * @return This period, for convenience. |
| */ |
| public Period set( |
| @Nullable Object id, |
| @Nullable Object uid, |
| int windowIndex, |
| long durationUs, |
| long positionInWindowUs) { |
| return set( |
| id, |
| uid, |
| windowIndex, |
| durationUs, |
| positionInWindowUs, |
| AdPlaybackState.NONE, |
| /* isPlaceholder= */ false); |
| } |
| |
| /** |
| * Sets the data held by this period. |
| * |
| * @param id An identifier for the period. Not necessarily unique. May be null if the ids of the |
| * period are not required. |
| * @param uid A unique identifier for the period. May be null if the ids of the period are not |
| * required. |
| * @param windowIndex The index of the window to which this period belongs. |
| * @param durationUs The duration of this period in microseconds, or {@link C#TIME_UNSET} if |
| * unknown. |
| * @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 adPlaybackState The state of the period's ads, or {@link AdPlaybackState#NONE} if |
| * there are no ads. |
| * @param isPlaceholder Whether this period contains placeholder information because the real |
| * information has yet to be loaded. |
| * @return This period, for convenience. |
| */ |
| public Period set( |
| @Nullable Object id, |
| @Nullable Object uid, |
| int windowIndex, |
| long durationUs, |
| long positionInWindowUs, |
| AdPlaybackState adPlaybackState, |
| boolean isPlaceholder) { |
| this.id = id; |
| this.uid = uid; |
| this.windowIndex = windowIndex; |
| this.durationUs = durationUs; |
| this.positionInWindowUs = positionInWindowUs; |
| this.adPlaybackState = adPlaybackState; |
| this.isPlaceholder = isPlaceholder; |
| return this; |
| } |
| |
| /** Returns the duration of the period in milliseconds, or {@link C#TIME_UNSET} if unknown. */ |
| public long getDurationMs() { |
| return Util.usToMs(durationUs); |
| } |
| |
| /** Returns the duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown. */ |
| public long getDurationUs() { |
| return durationUs; |
| } |
| |
| /** |
| * Returns 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. |
| */ |
| public long getPositionInWindowMs() { |
| return Util.usToMs(positionInWindowUs); |
| } |
| |
| /** |
| * Returns the position of the start of this period relative to the start of the window to which |
| * it belongs, in microseconds. May be negative if the start of the period is not within the |
| * window. |
| */ |
| public long getPositionInWindowUs() { |
| return positionInWindowUs; |
| } |
| |
| /** Returns the opaque identifier for ads played with this period, or {@code null} if unset. */ |
| @Nullable |
| public Object getAdsId() { |
| return adPlaybackState.adsId; |
| } |
| |
| /** Returns the number of ad groups in the period. */ |
| public int getAdGroupCount() { |
| return adPlaybackState.adGroupCount; |
| } |
| |
| /** |
| * Returns the number of removed ad groups in the period. Ad groups with indices between {@code |
| * 0} (inclusive) and {@code removedAdGroupCount} (exclusive) will be empty. |
| */ |
| public int getRemovedAdGroupCount() { |
| return adPlaybackState.removedAdGroupCount; |
| } |
| |
| /** |
| * Returns the time of the ad group at index {@code adGroupIndex} in the period, in |
| * microseconds. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return The time of the ad group at the index relative to the start of the enclosing {@link |
| * Period}, in microseconds, or {@link C#TIME_END_OF_SOURCE} for a post-roll ad group. |
| */ |
| public long getAdGroupTimeUs(int adGroupIndex) { |
| return adPlaybackState.getAdGroup(adGroupIndex).timeUs; |
| } |
| |
| /** |
| * 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 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 getFirstAdIndexToPlay(int adGroupIndex) { |
| return adPlaybackState.getAdGroup(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.getAdGroup(adGroupIndex).getNextAdIndexToPlay(lastPlayedAdIndex); |
| } |
| |
| /** |
| * Returns whether all ads in the ad group at index {@code adGroupIndex} have been played, |
| * skipped or failed. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return Whether all ads in the ad group at index {@code adGroupIndex} have been played, |
| * skipped or failed. |
| */ |
| public boolean hasPlayedAdGroup(int adGroupIndex) { |
| return !adPlaybackState.getAdGroup(adGroupIndex).hasUnplayedAds(); |
| } |
| |
| /** |
| * Returns the index of the ad group at or before {@code positionUs} in the period that should |
| * be played before the content at {@code positionUs}. 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 period 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) { |
| return adPlaybackState.getAdGroupIndexForPositionUs(positionUs, durationUs); |
| } |
| |
| /** |
| * Returns the index of the next ad group after {@code positionUs} in the period that has ads |
| * that should be played. Returns {@link C#INDEX_UNSET} if there is no such ad group. |
| * |
| * @param positionUs The period 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) { |
| return adPlaybackState.getAdGroupIndexAfterPositionUs(positionUs, durationUs); |
| } |
| |
| /** |
| * Returns the number of ads in the ad group at index {@code adGroupIndex}, or {@link |
| * C#LENGTH_UNSET} if not yet known. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return The number of ads in the ad group, or {@link C#LENGTH_UNSET} if not yet known. |
| */ |
| public int getAdCountInAdGroup(int adGroupIndex) { |
| return adPlaybackState.getAdGroup(adGroupIndex).count; |
| } |
| |
| /** |
| * Returns the duration of the ad at index {@code adIndexInAdGroup} in the ad group at {@code |
| * adGroupIndex}, in microseconds, or {@link C#TIME_UNSET} if not yet known. |
| * |
| * @param adGroupIndex The ad group index. |
| * @param adIndexInAdGroup The ad index in the ad group. |
| * @return The duration of the ad, or {@link C#TIME_UNSET} if not yet known. |
| */ |
| public long getAdDurationUs(int adGroupIndex, int adIndexInAdGroup) { |
| AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); |
| return adGroup.count != C.LENGTH_UNSET ? adGroup.durationsUs[adIndexInAdGroup] : C.TIME_UNSET; |
| } |
| |
| /** |
| * Returns the state of the ad at index {@code adIndexInAdGroup} in the ad group at {@code |
| * adGroupIndex}, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet known. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return The state of the ad, or {@link AdPlaybackState#AD_STATE_UNAVAILABLE} if not yet |
| * known. |
| */ |
| public int getAdState(int adGroupIndex, int adIndexInAdGroup) { |
| AdPlaybackState.AdGroup adGroup = adPlaybackState.getAdGroup(adGroupIndex); |
| return adGroup.count != C.LENGTH_UNSET |
| ? adGroup.states[adIndexInAdGroup] |
| : AD_STATE_UNAVAILABLE; |
| } |
| |
| /** |
| * Returns the position offset in the first unplayed ad at which to begin playback, in |
| * microseconds. |
| */ |
| public long getAdResumePositionUs() { |
| return adPlaybackState.adResumePositionUs; |
| } |
| |
| /** |
| * Returns whether the ad group at index {@code adGroupIndex} is server-side inserted and part |
| * of the content stream. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return Whether this ad group is server-side inserted and part of the content stream. |
| */ |
| public boolean isServerSideInsertedAdGroup(int adGroupIndex) { |
| return adPlaybackState.getAdGroup(adGroupIndex).isServerSideInserted; |
| } |
| |
| /** |
| * Returns the offset in microseconds which should be added to the content stream when resuming |
| * playback after the specified ad group. |
| * |
| * @param adGroupIndex The ad group index. |
| * @return The offset that should be added to the content stream, in microseconds. |
| */ |
| public long getContentResumeOffsetUs(int adGroupIndex) { |
| return adPlaybackState.getAdGroup(adGroupIndex).contentResumeOffsetUs; |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null || !getClass().equals(obj.getClass())) { |
| return false; |
| } |
| Period that = (Period) obj; |
| return Util.areEqual(id, that.id) |
| && Util.areEqual(uid, that.uid) |
| && windowIndex == that.windowIndex |
| && durationUs == that.durationUs |
| && positionInWindowUs == that.positionInWindowUs |
| && isPlaceholder == that.isPlaceholder |
| && Util.areEqual(adPlaybackState, that.adPlaybackState); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = 7; |
| result = 31 * result + (id == null ? 0 : id.hashCode()); |
| result = 31 * result + (uid == null ? 0 : uid.hashCode()); |
| result = 31 * result + windowIndex; |
| result = 31 * result + (int) (durationUs ^ (durationUs >>> 32)); |
| result = 31 * result + (int) (positionInWindowUs ^ (positionInWindowUs >>> 32)); |
| result = 31 * result + (isPlaceholder ? 1 : 0); |
| result = 31 * result + adPlaybackState.hashCode(); |
| return result; |
| } |
| |
| // Bundleable implementation. |
| |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({ |
| FIELD_WINDOW_INDEX, |
| FIELD_DURATION_US, |
| FIELD_POSITION_IN_WINDOW_US, |
| FIELD_PLACEHOLDER, |
| FIELD_AD_PLAYBACK_STATE |
| }) |
| private @interface FieldNumber {} |
| |
| private static final int FIELD_WINDOW_INDEX = 0; |
| private static final int FIELD_DURATION_US = 1; |
| private static final int FIELD_POSITION_IN_WINDOW_US = 2; |
| private static final int FIELD_PLACEHOLDER = 3; |
| private static final int FIELD_AD_PLAYBACK_STATE = 4; |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>It omits the {@link #id} and {@link #uid} fields so these fields of an instance restored |
| * by {@link #CREATOR} will always 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.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex); |
| bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); |
| bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs); |
| bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder); |
| bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle()); |
| return bundle; |
| } |
| |
| /** |
| * Object that can restore {@link Period} from a {@link Bundle}. |
| * |
| * <p>The {@link #id} and {@link #uid} of restored instances will always be {@code null}. |
| */ |
| public static final Creator<Period> CREATOR = Period::fromBundle; |
| |
| private static Period fromBundle(Bundle bundle) { |
| int windowIndex = bundle.getInt(keyForField(FIELD_WINDOW_INDEX), /* defaultValue= */ 0); |
| long durationUs = |
| bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET); |
| long positionInWindowUs = |
| bundle.getLong(keyForField(FIELD_POSITION_IN_WINDOW_US), /* defaultValue= */ 0); |
| boolean isPlaceholder = bundle.getBoolean(keyForField(FIELD_PLACEHOLDER)); |
| @Nullable |
| Bundle adPlaybackStateBundle = bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATE)); |
| AdPlaybackState adPlaybackState = |
| adPlaybackStateBundle != null |
| ? AdPlaybackState.CREATOR.fromBundle(adPlaybackStateBundle) |
| : AdPlaybackState.NONE; |
| |
| Period period = new Period(); |
| period.set( |
| /* id= */ null, |
| /* uid= */ null, |
| windowIndex, |
| durationUs, |
| positionInWindowUs, |
| adPlaybackState, |
| isPlaceholder); |
| return period; |
| } |
| |
| private static String keyForField(@Period.FieldNumber int field) { |
| return Integer.toString(field, Character.MAX_RADIX); |
| } |
| } |
| |
| /** An empty timeline. */ |
| public static final Timeline EMPTY = |
| new Timeline() { |
| |
| @Override |
| public int getWindowCount() { |
| return 0; |
| } |
| |
| @Override |
| public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| @Override |
| public int getPeriodCount() { |
| return 0; |
| } |
| |
| @Override |
| public Period getPeriod(int periodIndex, Period period, boolean setIds) { |
| throw new IndexOutOfBoundsException(); |
| } |
| |
| @Override |
| public int getIndexOfPeriod(Object uid) { |
| return C.INDEX_UNSET; |
| } |
| |
| @Override |
| public Object getUidOfPeriod(int periodIndex) { |
| throw new IndexOutOfBoundsException(); |
| } |
| }; |
| |
| protected Timeline() {} |
| |
| /** Returns whether the timeline is empty. */ |
| public final boolean isEmpty() { |
| return getWindowCount() == 0; |
| } |
| |
| /** Returns the number of windows in the timeline. */ |
| public abstract int getWindowCount(); |
| |
| /** |
| * Returns the index of the window after the window at index {@code windowIndex} depending on the |
| * {@code repeatMode} and whether shuffling is enabled. |
| * |
| * @param windowIndex Index of a window in the timeline. |
| * @param repeatMode A repeat mode. |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. |
| */ |
| public int getNextWindowIndex( |
| int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { |
| switch (repeatMode) { |
| case Player.REPEAT_MODE_OFF: |
| return windowIndex == getLastWindowIndex(shuffleModeEnabled) |
| ? C.INDEX_UNSET |
| : windowIndex + 1; |
| case Player.REPEAT_MODE_ONE: |
| return windowIndex; |
| case Player.REPEAT_MODE_ALL: |
| return windowIndex == getLastWindowIndex(shuffleModeEnabled) |
| ? getFirstWindowIndex(shuffleModeEnabled) |
| : windowIndex + 1; |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * Returns the index of the window before the window at index {@code windowIndex} depending on the |
| * {@code repeatMode} and whether shuffling is enabled. |
| * |
| * @param windowIndex Index of a window in the timeline. |
| * @param repeatMode A repeat mode. |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. |
| */ |
| public int getPreviousWindowIndex( |
| int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { |
| switch (repeatMode) { |
| case Player.REPEAT_MODE_OFF: |
| return windowIndex == getFirstWindowIndex(shuffleModeEnabled) |
| ? C.INDEX_UNSET |
| : windowIndex - 1; |
| case Player.REPEAT_MODE_ONE: |
| return windowIndex; |
| case Player.REPEAT_MODE_ALL: |
| return windowIndex == getFirstWindowIndex(shuffleModeEnabled) |
| ? getLastWindowIndex(shuffleModeEnabled) |
| : windowIndex - 1; |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * Returns the index of the last window in the playback order depending on whether shuffling is |
| * enabled. |
| * |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the |
| * timeline is empty. |
| */ |
| public int getLastWindowIndex(boolean shuffleModeEnabled) { |
| return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1; |
| } |
| |
| /** |
| * Returns the index of the first window in the playback order depending on whether shuffling is |
| * enabled. |
| * |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the |
| * timeline is empty. |
| */ |
| public int getFirstWindowIndex(boolean shuffleModeEnabled) { |
| return isEmpty() ? C.INDEX_UNSET : 0; |
| } |
| |
| /** |
| * Populates a {@link Window} with data for the window at the specified index. |
| * |
| * @param windowIndex The index of the window. |
| * @param window The {@link Window} to populate. Must not be null. |
| * @return The populated {@link Window}, for convenience. |
| */ |
| public final Window getWindow(int windowIndex, Window window) { |
| return getWindow(windowIndex, window, /* defaultPositionProjectionUs= */ 0); |
| } |
| |
| /** |
| * Populates a {@link Window} with data for the window at the specified index. |
| * |
| * @param windowIndex The index of the window. |
| * @param window The {@link Window} to populate. Must not be null. |
| * @param defaultPositionProjectionUs A duration into the future that the populated window's |
| * default start position should be projected. |
| * @return The populated {@link Window}, for convenience. |
| */ |
| public abstract Window getWindow( |
| int windowIndex, Window window, long defaultPositionProjectionUs); |
| |
| /** Returns the number of periods in the timeline. */ |
| public abstract int getPeriodCount(); |
| |
| /** |
| * Returns the index of the period after the period at index {@code periodIndex} depending on the |
| * {@code repeatMode} and whether shuffling is enabled. |
| * |
| * @param periodIndex Index of a period in the timeline. |
| * @param period A {@link Period} to be used internally. Must not be null. |
| * @param window A {@link Window} to be used internally. Must not be null. |
| * @param repeatMode A repeat mode. |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. |
| */ |
| public final int getNextPeriodIndex( |
| int periodIndex, |
| Period period, |
| Window window, |
| @Player.RepeatMode int repeatMode, |
| boolean shuffleModeEnabled) { |
| int windowIndex = getPeriod(periodIndex, period).windowIndex; |
| if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { |
| int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); |
| if (nextWindowIndex == C.INDEX_UNSET) { |
| return C.INDEX_UNSET; |
| } |
| return getWindow(nextWindowIndex, window).firstPeriodIndex; |
| } |
| return periodIndex + 1; |
| } |
| |
| /** |
| * Returns whether the given period is the last period of the timeline depending on the {@code |
| * repeatMode} and whether shuffling is enabled. |
| * |
| * @param periodIndex A period index. |
| * @param period A {@link Period} to be used internally. Must not be null. |
| * @param window A {@link Window} to be used internally. Must not be null. |
| * @param repeatMode A repeat mode. |
| * @param shuffleModeEnabled Whether shuffling is enabled. |
| * @return Whether the period of the given index is the last period of the timeline. |
| */ |
| public final boolean isLastPeriod( |
| int periodIndex, |
| Period period, |
| Window window, |
| @Player.RepeatMode int repeatMode, |
| boolean shuffleModeEnabled) { |
| return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled) |
| == C.INDEX_UNSET; |
| } |
| |
| /** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long)} instead. */ |
| @Deprecated |
| @InlineMe(replacement = "this.getPeriodPositionUs(window, period, windowIndex, windowPositionUs)") |
| public final Pair<Object, Long> getPeriodPosition( |
| Window window, Period period, int windowIndex, long windowPositionUs) { |
| return getPeriodPositionUs(window, period, windowIndex, windowPositionUs); |
| } |
| /** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long, long)} instead. */ |
| @Deprecated |
| @Nullable |
| @InlineMe( |
| replacement = |
| "this.getPeriodPositionUs(" |
| + "window, period, windowIndex, windowPositionUs, defaultPositionProjectionUs)") |
| public final Pair<Object, Long> getPeriodPosition( |
| Window window, |
| Period period, |
| int windowIndex, |
| long windowPositionUs, |
| long defaultPositionProjectionUs) { |
| return getPeriodPositionUs( |
| window, period, windowIndex, windowPositionUs, defaultPositionProjectionUs); |
| } |
| |
| /** |
| * Calls {@link #getPeriodPositionUs(Window, Period, int, long)} with a zero default position |
| * projection. |
| */ |
| public final Pair<Object, Long> getPeriodPositionUs( |
| Window window, Period period, int windowIndex, long windowPositionUs) { |
| return Assertions.checkNotNull( |
| getPeriodPositionUs( |
| window, period, windowIndex, windowPositionUs, /* defaultPositionProjectionUs= */ 0)); |
| } |
| |
| /** |
| * Converts {@code (windowIndex, windowPositionUs)} to the corresponding {@code (periodUid, |
| * periodPositionUs)}. The returned {@code periodPositionUs} is constrained to be non-negative, |
| * and to be less than the containing period's duration if it is known. |
| * |
| * @param window A {@link Window} that may be overwritten. |
| * @param period A {@link Period} that may be overwritten. |
| * @param windowIndex The window index. |
| * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default |
| * start position. |
| * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the |
| * duration into the future by which the window's position should be projected. |
| * @return The corresponding (periodUid, periodPositionUs), or null if {@code #windowPositionUs} |
| * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's |
| * position could not be projected by {@code defaultPositionProjectionUs}. |
| */ |
| @Nullable |
| public final Pair<Object, Long> getPeriodPositionUs( |
| Window window, |
| Period period, |
| int windowIndex, |
| long windowPositionUs, |
| long defaultPositionProjectionUs) { |
| Assertions.checkIndex(windowIndex, 0, getWindowCount()); |
| getWindow(windowIndex, window, defaultPositionProjectionUs); |
| if (windowPositionUs == C.TIME_UNSET) { |
| windowPositionUs = window.getDefaultPositionUs(); |
| if (windowPositionUs == C.TIME_UNSET) { |
| return null; |
| } |
| } |
| int periodIndex = window.firstPeriodIndex; |
| getPeriod(periodIndex, period); |
| while (periodIndex < window.lastPeriodIndex |
| && period.positionInWindowUs != windowPositionUs |
| && getPeriod(periodIndex + 1, period).positionInWindowUs <= windowPositionUs) { |
| periodIndex++; |
| } |
| getPeriod(periodIndex, period, /* setIds= */ true); |
| long periodPositionUs = windowPositionUs - period.positionInWindowUs; |
| // The period positions must be less than the period duration, if it is known. |
| if (period.durationUs != C.TIME_UNSET) { |
| periodPositionUs = min(periodPositionUs, period.durationUs - 1); |
| } |
| // Period positions cannot be negative. |
| periodPositionUs = max(0, periodPositionUs); |
| return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs); |
| } |
| |
| /** |
| * Populates a {@link Period} with data for the period with the specified unique identifier. |
| * |
| * @param periodUid The unique identifier of the period. |
| * @param period The {@link Period} to populate. Must not be null. |
| * @return The populated {@link Period}, for convenience. |
| */ |
| public Period getPeriodByUid(Object periodUid, Period period) { |
| return getPeriod(getIndexOfPeriod(periodUid), period, /* setIds= */ true); |
| } |
| |
| /** |
| * Populates a {@link Period} with data for the period at the specified index. {@link Period#id} |
| * and {@link Period#uid} will be set to null. |
| * |
| * @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. |
| * |
| * @param periodIndex The index of the period. |
| * @param period The {@link Period} to populate. Must not be null. |
| * @param setIds Whether {@link Period#id} and {@link Period#uid} should be populated. If false, |
| * the fields will be set to null. The caller should pass false for efficiency reasons unless |
| * the fields are required. |
| * @return The populated {@link Period}, for convenience. |
| */ |
| public abstract Period getPeriod(int periodIndex, Period period, boolean setIds); |
| |
| /** |
| * Returns the index of the period identified by its unique {@link Period#uid}, or {@link |
| * C#INDEX_UNSET} if the period is not in the timeline. |
| * |
| * @param uid A unique identifier for a period. |
| * @return The index of the period, or {@link C#INDEX_UNSET} if the period was not found. |
| */ |
| public abstract int getIndexOfPeriod(Object uid); |
| |
| /** |
| * Returns the unique id of the period identified by its index in the timeline. |
| * |
| * @param periodIndex The index of the period. |
| * @return The unique id of the period. |
| */ |
| public abstract Object getUidOfPeriod(int periodIndex); |
| |
| @Override |
| public boolean equals(@Nullable Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (!(obj instanceof Timeline)) { |
| return false; |
| } |
| Timeline other = (Timeline) obj; |
| if (other.getWindowCount() != getWindowCount() || other.getPeriodCount() != getPeriodCount()) { |
| return false; |
| } |
| Timeline.Window window = new Timeline.Window(); |
| Timeline.Period period = new Timeline.Period(); |
| Timeline.Window otherWindow = new Timeline.Window(); |
| Timeline.Period otherPeriod = new Timeline.Period(); |
| for (int i = 0; i < getWindowCount(); i++) { |
| if (!getWindow(i, window).equals(other.getWindow(i, otherWindow))) { |
| return false; |
| } |
| } |
| for (int i = 0; i < getPeriodCount(); i++) { |
| if (!getPeriod(i, period, /* setIds= */ true) |
| .equals(other.getPeriod(i, otherPeriod, /* setIds= */ true))) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public int hashCode() { |
| Window window = new Window(); |
| Period period = new Period(); |
| int result = 7; |
| result = 31 * result + getWindowCount(); |
| for (int i = 0; i < getWindowCount(); i++) { |
| result = 31 * result + getWindow(i, window).hashCode(); |
| } |
| result = 31 * result + getPeriodCount(); |
| for (int i = 0; i < getPeriodCount(); i++) { |
| result = 31 * result + getPeriod(i, period, /* setIds= */ true).hashCode(); |
| } |
| return result; |
| } |
| |
| // Bundleable implementation. |
| |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({ |
| FIELD_WINDOWS, |
| FIELD_PERIODS, |
| FIELD_SHUFFLED_WINDOW_INDICES, |
| }) |
| private @interface FieldNumber {} |
| |
| private static final int FIELD_WINDOWS = 0; |
| private static final int FIELD_PERIODS = 1; |
| private static final int FIELD_SHUFFLED_WINDOW_INDICES = 2; |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of |
| * an instance restored by {@link #CREATOR} may have missing fields as described in {@link |
| * Window#toBundle()} and {@link Period#toBundle()}. |
| * |
| * @param excludeMediaItems Whether to exclude all {@link Window#mediaItem media items} of windows |
| * in the timeline. |
| */ |
| public final Bundle toBundle(boolean excludeMediaItems) { |
| List<Bundle> windowBundles = new ArrayList<>(); |
| int windowCount = getWindowCount(); |
| Window window = new Window(); |
| for (int i = 0; i < windowCount; i++) { |
| windowBundles.add( |
| getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle(excludeMediaItems)); |
| } |
| |
| List<Bundle> periodBundles = new ArrayList<>(); |
| int periodCount = getPeriodCount(); |
| Period period = new Period(); |
| for (int i = 0; i < periodCount; i++) { |
| periodBundles.add(getPeriod(i, period, /* setIds= */ false).toBundle()); |
| } |
| |
| int[] shuffledWindowIndices = new int[windowCount]; |
| if (windowCount > 0) { |
| shuffledWindowIndices[0] = getFirstWindowIndex(/* shuffleModeEnabled= */ true); |
| } |
| for (int i = 1; i < windowCount; i++) { |
| shuffledWindowIndices[i] = |
| getNextWindowIndex( |
| shuffledWindowIndices[i - 1], Player.REPEAT_MODE_OFF, /* shuffleModeEnabled= */ true); |
| } |
| |
| Bundle bundle = new Bundle(); |
| BundleUtil.putBinder( |
| bundle, keyForField(FIELD_WINDOWS), new BundleListRetriever(windowBundles)); |
| BundleUtil.putBinder( |
| bundle, keyForField(FIELD_PERIODS), new BundleListRetriever(periodBundles)); |
| bundle.putIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES), shuffledWindowIndices); |
| return bundle; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of |
| * an instance restored by {@link #CREATOR} may have missing fields as described in {@link |
| * Window#toBundle()} and {@link Period#toBundle()}. |
| */ |
| @Override |
| public final Bundle toBundle() { |
| return toBundle(/* excludeMediaItems= */ false); |
| } |
| |
| /** |
| * Object that can restore a {@link Timeline} from a {@link Bundle}. |
| * |
| * <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of |
| * a restored instance may have missing fields as described in {@link Window#CREATOR} and {@link |
| * Period#CREATOR}. |
| */ |
| public static final Creator<Timeline> CREATOR = Timeline::fromBundle; |
| |
| private static Timeline fromBundle(Bundle bundle) { |
| ImmutableList<Window> windows = |
| fromBundleListRetriever( |
| Window.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_WINDOWS))); |
| ImmutableList<Period> periods = |
| fromBundleListRetriever( |
| Period.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_PERIODS))); |
| @Nullable |
| int[] shuffledWindowIndices = bundle.getIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES)); |
| return new RemotableTimeline( |
| windows, |
| periods, |
| shuffledWindowIndices == null |
| ? generateUnshuffledIndices(windows.size()) |
| : shuffledWindowIndices); |
| } |
| |
| private static <T extends Bundleable> ImmutableList<T> fromBundleListRetriever( |
| Creator<T> creator, @Nullable IBinder binder) { |
| if (binder == null) { |
| return ImmutableList.of(); |
| } |
| ImmutableList.Builder<T> builder = new ImmutableList.Builder<>(); |
| List<Bundle> bundleList = BundleListRetriever.getList(binder); |
| for (int i = 0; i < bundleList.size(); i++) { |
| builder.add(creator.fromBundle(bundleList.get(i))); |
| } |
| return builder.build(); |
| } |
| |
| private static String keyForField(@FieldNumber int field) { |
| return Integer.toString(field, Character.MAX_RADIX); |
| } |
| |
| private static int[] generateUnshuffledIndices(int n) { |
| int[] indices = new int[n]; |
| for (int i = 0; i < n; i++) { |
| indices[i] = i; |
| } |
| return indices; |
| } |
| |
| /** |
| * A concrete class of {@link Timeline} to restore a {@link Timeline} instance from a {@link |
| * Bundle} sent by another process via {@link IBinder}. |
| */ |
| public static final class RemotableTimeline extends Timeline { |
| |
| private final ImmutableList<Window> windows; |
| private final ImmutableList<Period> periods; |
| private final int[] shuffledWindowIndices; |
| private final int[] windowIndicesInShuffled; |
| |
| public RemotableTimeline( |
| ImmutableList<Window> windows, ImmutableList<Period> periods, int[] shuffledWindowIndices) { |
| checkArgument(windows.size() == shuffledWindowIndices.length); |
| this.windows = windows; |
| this.periods = periods; |
| this.shuffledWindowIndices = shuffledWindowIndices; |
| windowIndicesInShuffled = new int[shuffledWindowIndices.length]; |
| for (int i = 0; i < shuffledWindowIndices.length; i++) { |
| windowIndicesInShuffled[shuffledWindowIndices[i]] = i; |
| } |
| } |
| |
| @Override |
| public int getWindowCount() { |
| return windows.size(); |
| } |
| |
| @Override |
| public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { |
| Window w = windows.get(windowIndex); |
| window.set( |
| w.uid, |
| w.mediaItem, |
| w.manifest, |
| w.presentationStartTimeMs, |
| w.windowStartTimeMs, |
| w.elapsedRealtimeEpochOffsetMs, |
| w.isSeekable, |
| w.isDynamic, |
| w.liveConfiguration, |
| w.defaultPositionUs, |
| w.durationUs, |
| w.firstPeriodIndex, |
| w.lastPeriodIndex, |
| w.positionInFirstPeriodUs); |
| window.isPlaceholder = w.isPlaceholder; |
| return window; |
| } |
| |
| @Override |
| public int getNextWindowIndex( |
| int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { |
| if (repeatMode == Player.REPEAT_MODE_ONE) { |
| return windowIndex; |
| } |
| if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) { |
| return repeatMode == Player.REPEAT_MODE_ALL |
| ? getFirstWindowIndex(shuffleModeEnabled) |
| : C.INDEX_UNSET; |
| } |
| return shuffleModeEnabled |
| ? shuffledWindowIndices[windowIndicesInShuffled[windowIndex] + 1] |
| : windowIndex + 1; |
| } |
| |
| @Override |
| public int getPreviousWindowIndex( |
| int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { |
| if (repeatMode == Player.REPEAT_MODE_ONE) { |
| return windowIndex; |
| } |
| if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) { |
| return repeatMode == Player.REPEAT_MODE_ALL |
| ? getLastWindowIndex(shuffleModeEnabled) |
| : C.INDEX_UNSET; |
| } |
| return shuffleModeEnabled |
| ? shuffledWindowIndices[windowIndicesInShuffled[windowIndex] - 1] |
| : windowIndex - 1; |
| } |
| |
| @Override |
| public int getLastWindowIndex(boolean shuffleModeEnabled) { |
| if (isEmpty()) { |
| return C.INDEX_UNSET; |
| } |
| return shuffleModeEnabled |
| ? shuffledWindowIndices[getWindowCount() - 1] |
| : getWindowCount() - 1; |
| } |
| |
| @Override |
| public int getFirstWindowIndex(boolean shuffleModeEnabled) { |
| if (isEmpty()) { |
| return C.INDEX_UNSET; |
| } |
| return shuffleModeEnabled ? shuffledWindowIndices[0] : 0; |
| } |
| |
| @Override |
| public int getPeriodCount() { |
| return periods.size(); |
| } |
| |
| @Override |
| public Period getPeriod(int periodIndex, Period period, boolean setIds) { |
| Period p = periods.get(periodIndex); |
| period.set( |
| p.id, |
| p.uid, |
| p.windowIndex, |
| p.durationUs, |
| p.positionInWindowUs, |
| p.adPlaybackState, |
| p.isPlaceholder); |
| return period; |
| } |
| |
| @Override |
| public int getIndexOfPeriod(Object uid) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public Object getUidOfPeriod(int periodIndex) { |
| throw new UnsupportedOperationException(); |
| } |
| } |
| } |