/*
* Copyright (C) 2017 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 androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import java.io.IOException;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Media period that defers calling {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)}
* on a given source until {@link #createPeriod(MediaPeriodId)} has been called. This is useful if
* you need to return a media period immediately but the media source that should create it is not
* yet available or prepared.
*/
@UnstableApi
public final class MaskingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
/** Listener for preparation events. */
public interface PrepareListener {
/** Called when preparing the media period completes. */
void onPrepareComplete(MediaPeriodId mediaPeriodId);
/**
* Called the first time an error occurs while refreshing source info or preparing the period.
*/
void onPrepareError(MediaPeriodId mediaPeriodId, IOException exception);
}
/** The {@link MediaPeriodId} used to create the masking media period. */
public final MediaPeriodId id;
private final long preparePositionUs;
private final Allocator allocator;
/** The {@link MediaSource} that will create the underlying media period. */
private @MonotonicNonNull MediaSource mediaSource;
private @MonotonicNonNull MediaPeriod mediaPeriod;
@Nullable private Callback callback;
@Nullable private PrepareListener listener;
private boolean notifiedPrepareError;
private long preparePositionOverrideUs;
/**
* Creates a new masking media period. The media source must be set via {@link
* #setMediaSource(MediaSource)} before preparation can start.
*
* @param id The identifier used to create the masking media period.
* @param allocator The allocator used to create the media period.
* @param preparePositionUs The expected start position, in microseconds.
*/
public MaskingMediaPeriod(MediaPeriodId id, Allocator allocator, long preparePositionUs) {
this.id = id;
this.allocator = allocator;
this.preparePositionUs = preparePositionUs;
preparePositionOverrideUs = C.TIME_UNSET;
}
/**
* Sets a listener for preparation events.
*
* @param listener An listener to be notified of media period preparation events. If a listener is
* set, {@link #maybeThrowPrepareError()} will not throw but will instead pass the first
* preparation error (if any) to the listener.
*/
public void setPrepareListener(PrepareListener listener) {
this.listener = listener;
}
/** Returns the position at which the masking media period was prepared, in microseconds. */
public long getPreparePositionUs() {
return preparePositionUs;
}
/**
* Overrides the default prepare position at which to prepare the media period. This method must
* be called before {@link #createPeriod(MediaPeriodId)}.
*
* @param preparePositionUs The default prepare position to use, in microseconds.
*/
public void overridePreparePositionUs(long preparePositionUs) {
preparePositionOverrideUs = preparePositionUs;
}
/** Returns the prepare position override set by {@link #overridePreparePositionUs(long)}. */
public long getPreparePositionOverrideUs() {
return preparePositionOverrideUs;
}
/** Sets the {@link MediaSource} that will create the underlying media period. */
public void setMediaSource(MediaSource mediaSource) {
checkState(this.mediaSource == null);
this.mediaSource = mediaSource;
}
/**
* Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator, long)} on the wrapped source
* then prepares it if {@link #prepare(Callback, long)} has been called. Call {@link
* #releasePeriod()} to release the period.
*
* @param id The identifier that should be used to create the media period from the media source.
*/
public void createPeriod(MediaPeriodId id) {
long preparePositionUs = getPreparePositionWithOverride(this.preparePositionUs);
mediaPeriod = checkNotNull(mediaSource).createPeriod(id, allocator, preparePositionUs);
if (callback != null) {
mediaPeriod.prepare(/* callback= */ this, preparePositionUs);
}
}
/** Releases the period. */
public void releasePeriod() {
if (mediaPeriod != null) {
checkNotNull(mediaSource).releasePeriod(mediaPeriod);
}
}
@Override
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
if (mediaPeriod != null) {
mediaPeriod.prepare(
/* callback= */ this, getPreparePositionWithOverride(this.preparePositionUs));
}
}
@Override
public void maybeThrowPrepareError() throws IOException {
try {
if (mediaPeriod != null) {
mediaPeriod.maybeThrowPrepareError();
} else if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
} catch (IOException e) {
if (listener == null) {
throw e;
}
if (!notifiedPrepareError) {
notifiedPrepareError = true;
listener.onPrepareError(id, e);
}
}
}
@Override
public TrackGroupArray getTrackGroups() {
return castNonNull(mediaPeriod).getTrackGroups();
}
@Override
public long selectTracks(
@NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
if (preparePositionOverrideUs != C.TIME_UNSET && positionUs == preparePositionUs) {
positionUs = preparePositionOverrideUs;
preparePositionOverrideUs = C.TIME_UNSET;
}
return castNonNull(mediaPeriod)
.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs);
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
castNonNull(mediaPeriod).discardBuffer(positionUs, toKeyframe);
}
@Override
public long readDiscontinuity() {
return castNonNull(mediaPeriod).readDiscontinuity();
}
@Override
public long getBufferedPositionUs() {
return castNonNull(mediaPeriod).getBufferedPositionUs();
}
@Override
public long seekToUs(long positionUs) {
return castNonNull(mediaPeriod).seekToUs(positionUs);
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return castNonNull(mediaPeriod).getAdjustedSeekPositionUs(positionUs, seekParameters);
}
@Override
public long getNextLoadPositionUs() {
return castNonNull(mediaPeriod).getNextLoadPositionUs();
}
@Override
public void reevaluateBuffer(long positionUs) {
castNonNull(mediaPeriod).reevaluateBuffer(positionUs);
}
@Override
public boolean continueLoading(long positionUs) {
return mediaPeriod != null && mediaPeriod.continueLoading(positionUs);
}
@Override
public boolean isLoading() {
return mediaPeriod != null && mediaPeriod.isLoading();
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
castNonNull(callback).onContinueLoadingRequested(this);
}
// MediaPeriod.Callback implementation
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
castNonNull(callback).onPrepared(this);
if (listener != null) {
listener.onPrepareComplete(id);
}
}
private long getPreparePositionWithOverride(long preparePositionUs) {
return preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
: preparePositionUs;
}
}