public final class

MaskingMediaSource

extends WrappingMediaSource

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-exoplayer:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A MediaSource that masks the Timeline with a placeholder until the actual media structure is known.

Summary

Fields
from WrappingMediaSourcemediaSource
Constructors
publicMaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation)

Creates the masking media source.

Methods
public booleancanUpdateMediaItem(MediaItem mediaItem)

public MediaPeriodcreatePeriod(MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)

Creates the requested MediaPeriod.

protected MediaSource.MediaPeriodIdgetMediaPeriodIdForChildMediaPeriodId(MediaSource.MediaPeriodId mediaPeriodId)

Returns the in the wrapping source corresponding to the specified in a child source.

public TimelinegetTimeline()

Returns the Timeline.

public voidmaybeThrowSourceInfoRefreshError()

protected voidonChildSourceInfoRefreshed(Timeline newTimeline)

Called when the child source info has been refreshed.

protected voidprepareSourceInternal()

Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId).

public voidreleasePeriod(MediaPeriod mediaPeriod)

Releases a MediaPeriod.

protected abstract voidreleaseSourceInternal()

Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller).

public voidupdateMediaItem(MediaItem mediaItem)

from WrappingMediaSourcedisableChildSource, enableChildSource, getInitialTimeline, getMediaItem, getMediaPeriodIdForChildMediaPeriodId, getMediaTimeForChildMediaTime, getMediaTimeForChildMediaTime, getWindowIndexForChildWindowIndex, getWindowIndexForChildWindowIndex, isSingleWindow, onChildSourceInfoRefreshed, prepareChildSource, prepareSourceInternal, releaseChildSource
from CompositeMediaSource<T>disableChildSource, disableInternal, enableChildSource, enableInternal, getMediaPeriodIdForChildMediaPeriodId, getMediaTimeForChildMediaTime, getWindowIndexForChildWindowIndex, onChildSourceInfoRefreshed, prepareChildSource, releaseChildSource
from BaseMediaSourceaddDrmEventListener, addEventListener, createDrmEventDispatcher, createDrmEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, disable, enable, getPlayerId, isEnabled, prepareSource, prepareSource, prepareSourceCalled, refreshSourceInfo, releaseSource, removeDrmEventListener, removeEventListener, setPlayerId
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation)

Creates the masking media source.

Parameters:

mediaSource: A MediaSource.
useLazyPreparation: Whether the mediaSource is prepared lazily. If false, all manifest loads and other initial preparation steps happen immediately. If true, these initial preparations are triggered only when the player starts buffering the media.

Methods

public Timeline getTimeline()

Returns the Timeline.

public boolean canUpdateMediaItem(MediaItem mediaItem)

This method can be overridden to change whether the MediaItem of the child source can be updated.

public void updateMediaItem(MediaItem mediaItem)

This method can be overridden to change how the MediaItem of the child source is updated.

protected void prepareSourceInternal()

Starts source preparation and enables the source, see MediaSource.prepareSource(MediaSource.MediaSourceCaller, TransferListener, PlayerId). This method is called at most once until the next call to CompositeMediaSource.releaseSourceInternal().

public void maybeThrowSourceInfoRefreshError()

public MediaPeriod createPeriod(MediaSource.MediaPeriodId id, Allocator allocator, long startPositionUs)

Creates the requested MediaPeriod.

This method typically forwards to the wrapped media source and optionally wraps the returned MediaPeriod.

See also: MediaSource.createPeriod(MediaSource.MediaPeriodId, Allocator, long)

public void releasePeriod(MediaPeriod mediaPeriod)

Releases a MediaPeriod.

This method typically forwards to the wrapped media source and optionally unwraps the provided MediaPeriod.

See also: MediaSource.releasePeriod(MediaPeriod)

protected abstract void releaseSourceInternal()

Releases the source, see MediaSource.releaseSource(MediaSource.MediaSourceCaller). This method is called exactly once after each call to BaseMediaSource.prepareSourceInternal(TransferListener).

protected void onChildSourceInfoRefreshed(Timeline newTimeline)

Called when the child source info has been refreshed.

This Timeline can be amended if needed, for example using ForwardingTimeline. The Timeline for the wrapping source needs to be published with BaseMediaSource.refreshSourceInfo(Timeline).

Parameters:

newTimeline: The timeline of the child source.

protected MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaSource.MediaPeriodId mediaPeriodId)

Returns the in the wrapping source corresponding to the specified in a child source. The default implementation does not change the media period id.

Parameters:

mediaPeriodId: A of the child source.

Returns:

The corresponding in the wrapping source. Null if no corresponding media period id can be determined.

Source

/*
 * Copyright (C) 2019 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 androidx.media3.exoplayer.source;

import static java.lang.Math.max;

import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Window;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.upstream.Allocator;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;

/**
 * A {@link MediaSource} that masks the {@link Timeline} with a placeholder until the actual media
 * structure is known.
 */
@UnstableApi
public final class MaskingMediaSource extends WrappingMediaSource {

  private final boolean useLazyPreparation;
  private final Timeline.Window window;
  private final Timeline.Period period;

  private MaskingTimeline timeline;
  @Nullable private MaskingMediaPeriod unpreparedMaskingMediaPeriod;
  private boolean hasStartedPreparing;
  private boolean isPrepared;
  private boolean hasRealTimeline;

  /**
   * Creates the masking media source.
   *
   * @param mediaSource A {@link MediaSource}.
   * @param useLazyPreparation Whether the {@code mediaSource} is prepared lazily. If false, all
   *     manifest loads and other initial preparation steps happen immediately. If true, these
   *     initial preparations are triggered only when the player starts buffering the media.
   */
  public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
    super(mediaSource);
    this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow();
    window = new Timeline.Window();
    period = new Timeline.Period();
    @Nullable Timeline initialTimeline = mediaSource.getInitialTimeline();
    if (initialTimeline != null) {
      timeline =
          MaskingTimeline.createWithRealTimeline(
              initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null);
      hasRealTimeline = true;
    } else {
      timeline = MaskingTimeline.createWithPlaceholderTimeline(mediaSource.getMediaItem());
    }
  }

  /** Returns the {@link Timeline}. */
  public Timeline getTimeline() {
    return timeline;
  }

  @Override
  public boolean canUpdateMediaItem(MediaItem mediaItem) {
    return mediaSource.canUpdateMediaItem(mediaItem);
  }

  @Override
  public void updateMediaItem(MediaItem mediaItem) {
    if (hasRealTimeline) {
      timeline =
          timeline.cloneWithUpdatedTimeline(
              new TimelineWithUpdatedMediaItem(timeline.timeline, mediaItem));
    } else {
      timeline = MaskingTimeline.createWithPlaceholderTimeline(mediaItem);
    }
    mediaSource.updateMediaItem(mediaItem);
  }

  @Override
  public void prepareSourceInternal() {
    if (!useLazyPreparation) {
      hasStartedPreparing = true;
      prepareChildSource();
    }
  }

  @Override
  @SuppressWarnings("MissingSuperCall")
  public void maybeThrowSourceInfoRefreshError() {
    // Do nothing. Source info refresh errors will be thrown when calling
    // MaskingMediaPeriod.maybeThrowPrepareError.
  }

  @Override
  public MaskingMediaPeriod createPeriod(
      MediaPeriodId id, Allocator allocator, long startPositionUs) {
    MaskingMediaPeriod mediaPeriod = new MaskingMediaPeriod(id, allocator, startPositionUs);
    mediaPeriod.setMediaSource(mediaSource);
    if (isPrepared) {
      MediaPeriodId idInSource = id.copyWithPeriodUid(getInternalPeriodUid(id.periodUid));
      mediaPeriod.createPeriod(idInSource);
    } else {
      // We should have at most one media period while source is unprepared because the duration is
      // unset and we don't load beyond periods with unset duration. We need to figure out how to
      // handle the prepare positions of multiple deferred media periods, should that ever change.
      unpreparedMaskingMediaPeriod = mediaPeriod;
      if (!hasStartedPreparing) {
        hasStartedPreparing = true;
        prepareChildSource();
      }
    }
    return mediaPeriod;
  }

  @Override
  public void releasePeriod(MediaPeriod mediaPeriod) {
    ((MaskingMediaPeriod) mediaPeriod).releasePeriod();
    if (mediaPeriod == unpreparedMaskingMediaPeriod) {
      unpreparedMaskingMediaPeriod = null;
    }
  }

  @Override
  public void releaseSourceInternal() {
    isPrepared = false;
    hasStartedPreparing = false;
    super.releaseSourceInternal();
  }

  @Override
  protected void onChildSourceInfoRefreshed(Timeline newTimeline) {
    @Nullable MediaPeriodId idForMaskingPeriodPreparation = null;
    if (isPrepared) {
      timeline = timeline.cloneWithUpdatedTimeline(newTimeline);
      if (unpreparedMaskingMediaPeriod != null) {
        // Reset override in case the duration changed and we need to update our override.
        setPreparePositionOverrideToUnpreparedMaskingPeriod(
            unpreparedMaskingMediaPeriod.getPreparePositionOverrideUs());
      }
    } else if (newTimeline.isEmpty()) {
      timeline =
          hasRealTimeline
              ? timeline.cloneWithUpdatedTimeline(newTimeline)
              : MaskingTimeline.createWithRealTimeline(
                  newTimeline,
                  Window.SINGLE_WINDOW_UID,
                  MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID);
    } else {
      // Determine first period and the start position.
      // This will be:
      //  1. The default window start position if no deferred period has been created yet.
      //  2. The non-zero prepare position of the deferred period under the assumption that this is
      //     a non-zero initial seek position in the window.
      //  3. The default window start position if the deferred period has a prepare position of zero
      //     under the assumption that the prepare position of zero was used because it's the
      //     default position of the PlaceholderTimeline window. Note that this will override an
      //     intentional seek to zero for a window with a non-zero default position. This is
      //     unlikely to be a problem as a non-zero default position usually only occurs for live
      //     playbacks and seeking to zero in a live window would cause BehindLiveWindowExceptions
      //     anyway.
      newTimeline.getWindow(/* windowIndex= */ 0, window);
      long windowStartPositionUs = window.getDefaultPositionUs();
      Object windowUid = window.uid;
      if (unpreparedMaskingMediaPeriod != null) {
        long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
        timeline.getPeriodByUid(unpreparedMaskingMediaPeriod.id.periodUid, period);
        long windowPreparePositionUs = period.getPositionInWindowUs() + periodPreparePositionUs;
        long oldWindowDefaultPositionUs =
            timeline.getWindow(/* windowIndex= */ 0, window).getDefaultPositionUs();
        if (windowPreparePositionUs != oldWindowDefaultPositionUs) {
          windowStartPositionUs = windowPreparePositionUs;
        }
      }
      Pair<Object, Long> periodUidAndPositionUs =
          newTimeline.getPeriodPositionUs(
              window, period, /* windowIndex= */ 0, windowStartPositionUs);
      Object periodUid = periodUidAndPositionUs.first;
      long periodPositionUs = periodUidAndPositionUs.second;
      timeline =
          hasRealTimeline
              ? timeline.cloneWithUpdatedTimeline(newTimeline)
              : MaskingTimeline.createWithRealTimeline(newTimeline, windowUid, periodUid);
      if (unpreparedMaskingMediaPeriod != null) {
        MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
        if (setPreparePositionOverrideToUnpreparedMaskingPeriod(periodPositionUs)) {
          idForMaskingPeriodPreparation =
              maskingPeriod.id.copyWithPeriodUid(getInternalPeriodUid(maskingPeriod.id.periodUid));
        }
      }
    }
    hasRealTimeline = true;
    isPrepared = true;
    refreshSourceInfo(this.timeline);
    if (idForMaskingPeriodPreparation != null) {
      Assertions.checkNotNull(unpreparedMaskingMediaPeriod)
          .createPeriod(idForMaskingPeriodPreparation);
    }
  }

  @Override
  @Nullable
  protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(MediaPeriodId mediaPeriodId) {
    return mediaPeriodId.copyWithPeriodUid(getExternalPeriodUid(mediaPeriodId.periodUid));
  }

  private Object getInternalPeriodUid(Object externalPeriodUid) {
    return timeline.replacedInternalPeriodUid != null
            && externalPeriodUid.equals(MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID)
        ? timeline.replacedInternalPeriodUid
        : externalPeriodUid;
  }

  private Object getExternalPeriodUid(Object internalPeriodUid) {
    return timeline.replacedInternalPeriodUid != null
            && timeline.replacedInternalPeriodUid.equals(internalPeriodUid)
        ? MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID
        : internalPeriodUid;
  }

  @RequiresNonNull("unpreparedMaskingMediaPeriod")
  private boolean setPreparePositionOverrideToUnpreparedMaskingPeriod(
      long preparePositionOverrideUs) {
    MaskingMediaPeriod maskingPeriod = unpreparedMaskingMediaPeriod;
    int maskingPeriodIndex = timeline.getIndexOfPeriod(maskingPeriod.id.periodUid);
    if (maskingPeriodIndex == C.INDEX_UNSET) {
      // The new timeline doesn't contain this period anymore. This can happen if the media source
      // has multiple periods and removed the first period with a timeline update. Ignore the
      // update, as the non-existing period will be released anyway as soon as the player receives
      // this new timeline.
      return false;
    }
    long periodDurationUs = timeline.getPeriod(maskingPeriodIndex, period).durationUs;
    if (periodDurationUs != C.TIME_UNSET) {
      // Ensure the overridden position doesn't exceed the period duration.
      if (preparePositionOverrideUs >= periodDurationUs) {
        preparePositionOverrideUs = max(0, periodDurationUs - 1);
      }
    }
    maskingPeriod.overridePreparePositionUs(preparePositionOverrideUs);
    return true;
  }

  /**
   * Timeline used as placeholder for an unprepared media source. After preparation, a
   * MaskingTimeline is used to keep the originally assigned masking period ID.
   */
  private static final class MaskingTimeline extends ForwardingTimeline {

    public static final Object MASKING_EXTERNAL_PERIOD_UID = new Object();

    @Nullable private final Object replacedInternalWindowUid;
    @Nullable private final Object replacedInternalPeriodUid;

    /**
     * Returns an instance with a placeholder timeline using the provided {@link MediaItem}.
     *
     * @param mediaItem A {@link MediaItem}.
     */
    public static MaskingTimeline createWithPlaceholderTimeline(MediaItem mediaItem) {
      return new MaskingTimeline(
          new PlaceholderTimeline(mediaItem),
          Window.SINGLE_WINDOW_UID,
          MASKING_EXTERNAL_PERIOD_UID);
    }

    /**
     * Returns an instance with a real timeline, replacing the provided period ID with the already
     * assigned masking period ID.
     *
     * @param timeline The real timeline.
     * @param firstWindowUid The window UID in the timeline which will be replaced by the already
     *     assigned {@link Window#SINGLE_WINDOW_UID}.
     * @param firstPeriodUid The period UID in the timeline which will be replaced by the already
     *     assigned {@link #MASKING_EXTERNAL_PERIOD_UID}.
     */
    public static MaskingTimeline createWithRealTimeline(
        Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) {
      return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
    }

    private MaskingTimeline(
        Timeline timeline,
        @Nullable Object replacedInternalWindowUid,
        @Nullable Object replacedInternalPeriodUid) {
      super(timeline);
      this.replacedInternalWindowUid = replacedInternalWindowUid;
      this.replacedInternalPeriodUid = replacedInternalPeriodUid;
    }

    /**
     * Returns a copy with an updated timeline. This keeps the existing period replacement.
     *
     * @param timeline The new timeline.
     */
    public MaskingTimeline cloneWithUpdatedTimeline(Timeline timeline) {
      return new MaskingTimeline(timeline, replacedInternalWindowUid, replacedInternalPeriodUid);
    }

    @Override
    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
      timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
      if (Util.areEqual(window.uid, replacedInternalWindowUid)) {
        window.uid = Window.SINGLE_WINDOW_UID;
      }
      return window;
    }

    @Override
    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
      timeline.getPeriod(periodIndex, period, setIds);
      if (Util.areEqual(period.uid, replacedInternalPeriodUid) && setIds) {
        period.uid = MASKING_EXTERNAL_PERIOD_UID;
      }
      return period;
    }

    @Override
    public int getIndexOfPeriod(Object uid) {
      return timeline.getIndexOfPeriod(
          MASKING_EXTERNAL_PERIOD_UID.equals(uid) && replacedInternalPeriodUid != null
              ? replacedInternalPeriodUid
              : uid);
    }

    @Override
    public Object getUidOfPeriod(int periodIndex) {
      Object uid = timeline.getUidOfPeriod(periodIndex);
      return Util.areEqual(uid, replacedInternalPeriodUid) ? MASKING_EXTERNAL_PERIOD_UID : uid;
    }
  }

  /** A timeline with one dynamic window with a period of indeterminate duration. */
  @VisibleForTesting
  public static final class PlaceholderTimeline extends Timeline {

    private final MediaItem mediaItem;

    /** Creates a new instance with the given media item. */
    public PlaceholderTimeline(MediaItem mediaItem) {
      this.mediaItem = mediaItem;
    }

    @Override
    public int getWindowCount() {
      return 1;
    }

    @Override
    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
      window.set(
          Window.SINGLE_WINDOW_UID,
          mediaItem,
          /* manifest= */ null,
          /* presentationStartTimeMs= */ C.TIME_UNSET,
          /* windowStartTimeMs= */ C.TIME_UNSET,
          /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
          /* isSeekable= */ false,
          // Dynamic window to indicate pending timeline updates.
          /* isDynamic= */ true,
          /* liveConfiguration= */ null,
          /* defaultPositionUs= */ 0,
          /* durationUs= */ C.TIME_UNSET,
          /* firstPeriodIndex= */ 0,
          /* lastPeriodIndex= */ 0,
          /* positionInFirstPeriodUs= */ 0);
      window.isPlaceholder = true;
      return window;
    }

    @Override
    public int getPeriodCount() {
      return 1;
    }

    @Override
    public Period getPeriod(int periodIndex, Period period, boolean setIds) {
      period.set(
          /* id= */ setIds ? 0 : null,
          /* uid= */ setIds ? MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID : null,
          /* windowIndex= */ 0,
          /* durationUs= */ C.TIME_UNSET,
          /* positionInWindowUs= */ 0,
          /* adPlaybackState= */ AdPlaybackState.NONE,
          /* isPlaceholder= */ true);
      return period;
    }

    @Override
    public int getIndexOfPeriod(Object uid) {
      return uid == MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID ? 0 : C.INDEX_UNSET;
    }

    @Override
    public Object getUidOfPeriod(int periodIndex) {
      return MaskingTimeline.MASKING_EXTERNAL_PERIOD_UID;
    }
  }
}