public final class

MaskingMediaSource

extends CompositeMediaSource<java.lang.Void>

 java.lang.Object

androidx.media3.exoplayer.source.BaseMediaSource

androidx.media3.exoplayer.source.CompositeMediaSource<java.lang.Void>

↳androidx.media3.exoplayer.source.MaskingMediaSource

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-exoplayer', version: '1.0.0-alpha03'

  • groupId: androidx.media3
  • artifactId: media3-exoplayer
  • version: 1.0.0-alpha03

Artifact androidx.media3:media3-exoplayer:1.0.0-alpha03 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

Constructors
publicMaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation)

Creates the masking media source.

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

public MediaItemgetMediaItem()

protected MediaSource.MediaPeriodIdgetMediaPeriodIdForChildMediaPeriodId(java.lang.Object id, MediaSource.MediaPeriodId mediaPeriodId)

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

public TimelinegetTimeline()

Returns the Timeline.

public voidmaybeThrowSourceInfoRefreshError()

protected abstract voidonChildSourceInfoRefreshed(java.lang.Object id, MediaSource mediaSource, Timeline timeline)

Called when the source info of a child source has been refreshed.

protected abstract voidprepareSourceInternal(TransferListener mediaTransferListener)

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

public voidreleasePeriod(MediaPeriod mediaPeriod)

protected abstract voidreleaseSourceInternal()

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

from CompositeMediaSource<T>disableChildSource, disableInternal, enableChildSource, enableInternal, getMediaTimeForChildMediaTime, getWindowIndexForChildWindowIndex, prepareChildSource, releaseChildSource
from BaseMediaSourceaddDrmEventListener, addEventListener, createDrmEventDispatcher, createDrmEventDispatcher, createEventDispatcher, createEventDispatcher, createEventDispatcher, disable, enable, getPlayerId, isEnabled, prepareSource, refreshSourceInfo, releaseSource, removeDrmEventListener, removeEventListener
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.

protected abstract void prepareSourceInternal(TransferListener mediaTransferListener)

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 BaseMediaSource.releaseSourceInternal().

Parameters:

mediaTransferListener: The transfer listener which should be informed of any media data transfers. May be null if no listener is available. Note that this listener should usually be only informed of transfers related to the media loads and not of auxiliary loads for manifests and other data.

public MediaItem getMediaItem()

public void maybeThrowSourceInfoRefreshError()

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

public void releasePeriod(MediaPeriod 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 abstract void onChildSourceInfoRefreshed(java.lang.Object id, MediaSource mediaSource, Timeline timeline)

Called when the source info of a child source has been refreshed.

Parameters:

id: The unique id used to prepare the child source.
mediaSource: The child source whose source info has been refreshed.
timeline: The timeline of the child source.

protected MediaSource.MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(java.lang.Object id, MediaSource.MediaPeriodId mediaPeriodId)

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

Parameters:

id: The unique id used to prepare the child source.
mediaPeriodId: A of the child source.

Returns:

The corresponding in the composite 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.datasource.TransferListener;
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 CompositeMediaSource<Void> {

  private final MediaSource mediaSource;
  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) {
    this.mediaSource = 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 void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
    super.prepareSourceInternal(mediaTransferListener);
    if (!useLazyPreparation) {
      hasStartedPreparing = true;
      prepareChildSource(/* id= */ null, mediaSource);
    }
  }

  @Override
  public MediaItem getMediaItem() {
    return mediaSource.getMediaItem();
  }

  @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(/* id= */ null, mediaSource);
      }
    }
    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(
      Void id, MediaSource mediaSource, 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;
        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(
      Void id, 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 void 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;
    }
    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);
  }

  /**
   * 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);
    }

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

    @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;
    }
  }
}