Abstract EPII renderer logic into holder class

Move EPII calls to renderers to a separate managing class.

PiperOrigin-RevId: 688932712
This commit is contained in:
michaelkatz 2024-10-23 06:01:15 -07:00 committed by Copybara-Service
parent e677c8dccd
commit e5133e78f5
2 changed files with 531 additions and 157 deletions

View File

@ -19,9 +19,6 @@ 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 static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
import static androidx.media3.exoplayer.Renderer.STATE_ENABLED;
import static androidx.media3.exoplayer.Renderer.STATE_STARTED;
import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED;
import static java.lang.Math.max;
import static java.lang.Math.min;
@ -59,26 +56,21 @@ import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.metadata.MetadataRenderer;
import androidx.media3.exoplayer.source.BehindLiveWindowException;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.text.TextRenderer;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/** Implements the internal behavior of {@link ExoPlayerImpl}. */
@ -181,8 +173,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
private static final long PLAYBACK_BUFFER_EMPTY_THRESHOLD_US = 500_000;
private final Renderer[] renderers;
private final Set<Renderer> renderersToReset;
private final RendererHolder[] renderers;
private final RendererCapabilities[] rendererCapabilities;
private final boolean[] rendererReportedReady;
private final TrackSelector trackSelector;
@ -258,7 +249,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Nullable PlaybackLooperProvider playbackLooperProvider,
PreloadConfiguration preloadConfiguration) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.renderers = renderers;
this.trackSelector = trackSelector;
this.emptyTrackSelectorResult = emptyTrackSelectorResult;
this.loadControl = loadControl;
@ -289,16 +279,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Nullable
RendererCapabilities.Listener rendererCapabilitiesListener =
trackSelector.getRendererCapabilitiesListener();
this.renderers = new RendererHolder[renderers.length];
for (int i = 0; i < renderers.length; i++) {
renderers[i].init(/* index= */ i, playerId, clock);
rendererCapabilities[i] = renderers[i].getCapabilities();
if (rendererCapabilitiesListener != null) {
rendererCapabilities[i].setListener(rendererCapabilitiesListener);
}
this.renderers[i] = new RendererHolder(renderers[i], /* index= */ i);
}
mediaClock = new DefaultMediaClock(this, clock);
pendingMessages = new ArrayList<>();
renderersToReset = Sets.newIdentityHashSet();
window = new Timeline.Window();
period = new Timeline.Period();
trackSelector.init(/* listener= */ this, bandwidthMeter);
@ -981,18 +973,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
TrackSelectorResult trackSelectorResult = playingPeriodHolder.getTrackSelectorResult();
for (int i = 0; i < renderers.length; i++) {
if (trackSelectorResult.isRendererEnabled(i) && renderers[i].getState() == STATE_ENABLED) {
renderers[i].start();
if (!trackSelectorResult.isRendererEnabled(i)) {
continue;
}
renderers[i].start();
}
}
private void stopRenderers() throws ExoPlaybackException {
mediaClock.stop();
for (Renderer renderer : renderers) {
if (isRendererEnabled(renderer)) {
ensureStopped(renderer);
}
for (RendererHolder rendererHolder : renderers) {
rendererHolder.stop();
}
}
@ -1127,8 +1118,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
playingPeriodHolder.mediaPeriod.discardBuffer(
playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe);
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
if (!isRendererEnabled(renderer)) {
RendererHolder renderer = renderers[i];
if (renderer.getEnabledRendererCount() == 0) {
maybeTriggerOnRendererReadyChanged(/* rendererIndex= */ i, /* allowsPlayback= */ false);
continue;
}
@ -1136,16 +1127,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
// again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
// Determine whether the renderer allows playback to continue. Playback can
// continue if the renderer is ready or ended. Also continue playback if the renderer is
// reading ahead into the next stream or is waiting for the next stream. This is to avoid
// getting stuck if tracks in the current period have uneven durations and are still being
// read by another renderer. See: https://github.com/google/ExoPlayer/issues/1874.
renderersEnded = renderersEnded && renderer.isEnded();
// Determine whether the renderer allows playback to continue. Playback can continue if the
// renderer is ready or ended. Also continue playback if the renderer is reading ahead into
// the next stream or is waiting for the next stream. This is to avoid getting stuck if
// tracks in the current period have uneven durations and are still being read by another
// renderer. See: https://github.com/google/ExoPlayer/issues/1874.
boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream();
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
boolean allowsPlayback =
isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
boolean allowsPlayback = renderer.allowsPlayback(playingPeriodHolder);
maybeTriggerOnRendererReadyChanged(/* rendererIndex= */ i, allowsPlayback);
renderersAllowPlayback = renderersAllowPlayback && allowsPlayback;
if (!allowsPlayback) {
@ -1198,8 +1186,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean playbackMaybeStuck = false;
if (playbackInfo.playbackState == Player.STATE_BUFFERING) {
for (int i = 0; i < renderers.length; i++) {
if (isRendererEnabled(renderers[i])
&& renderers[i].getStream() == playingPeriodHolder.sampleStreams[i]) {
if (renderers[i].isReadingFromPeriod(playingPeriodHolder)) {
maybeThrowRendererStreamError(/* rendererIndex= */ i);
}
}
@ -1285,15 +1272,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
? READY_MAXIMUM_INTERVAL_MS
: BUFFERING_MAXIMUM_INTERVAL_MS;
if (dynamicSchedulingEnabled && shouldPlayWhenReady()) {
for (Renderer renderer : renderers) {
if (isRendererEnabled(renderer)) {
wakeUpTimeIntervalMs =
min(
wakeUpTimeIntervalMs,
Util.usToMs(
renderer.getDurationToProgressUs(
rendererPositionUs, rendererPositionElapsedRealtimeUs)));
}
for (RendererHolder rendererHolder : renderers) {
wakeUpTimeIntervalMs =
min(
wakeUpTimeIntervalMs,
Util.usToMs(
rendererHolder.getMinDurationToProgressUs(
rendererPositionUs, rendererPositionElapsedRealtimeUs)));
}
}
handler.sendEmptyMessageAtTime(
@ -1448,9 +1433,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|| oldPlayingPeriodHolder != newPlayingPeriodHolder
|| (newPlayingPeriodHolder != null
&& newPlayingPeriodHolder.toRendererTime(periodPositionUs) < 0)) {
for (int i = 0; i < renderers.length; i++) {
disableRenderer(/* rendererIndex= */ i);
}
disableRenderers();
if (newPlayingPeriodHolder != null) {
// Update the queue and reenable renderers if the requested media period already exists.
while (queue.getPlayingPeriod() != newPlayingPeriodHolder) {
@ -1494,10 +1477,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
? MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US + periodPositionUs
: playingMediaPeriod.toRendererTime(periodPositionUs);
mediaClock.resetPosition(rendererPositionUs);
for (Renderer renderer : renderers) {
if (isRendererEnabled(renderer)) {
renderer.resetPosition(rendererPositionUs);
}
for (RendererHolder rendererHolder : renderers) {
rendererHolder.resetPosition(rendererPositionUs);
}
notifyTrackSelectionDiscontinuity();
}
@ -1517,9 +1498,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (this.foregroundMode != foregroundMode) {
this.foregroundMode = foregroundMode;
if (!foregroundMode) {
for (Renderer renderer : renderers) {
if (!isRendererEnabled(renderer) && renderersToReset.remove(renderer)) {
renderer.reset();
for (RendererHolder rendererHolder : renderers) {
if (rendererHolder.getEnabledRendererCount() == 0) {
rendererHolder.reset();
}
}
}
@ -1572,23 +1553,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
updateRebufferingState(/* isRebuffering= */ false, /* resetLastRebufferRealtimeMs= */ true);
mediaClock.stop();
rendererPositionUs = MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US;
for (int i = 0; i < renderers.length; i++) {
try {
disableRenderer(/* rendererIndex= */ i);
} catch (ExoPlaybackException | RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Disable failed.", e);
}
try {
disableRenderers();
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Disable failed.", e);
}
if (resetRenderers) {
for (Renderer renderer : renderers) {
if (renderersToReset.remove(renderer)) {
try {
renderer.reset();
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Reset failed.", e);
}
for (RendererHolder rendererHolder : renderers) {
try {
rendererHolder.reset();
} catch (RuntimeException e) {
// There's nothing we can do.
Log.e(TAG, "Reset failed.", e);
}
}
}
@ -1842,22 +1819,17 @@ import java.util.concurrent.atomic.AtomicBoolean;
nextPendingMessageIndexHint = nextPendingMessageIndex;
}
private void ensureStopped(Renderer renderer) {
if (renderer.getState() == STATE_STARTED) {
renderer.stop();
private void disableRenderers() {
for (int i = 0; i < renderers.length; i++) {
disableRenderer(i);
}
}
private void disableRenderer(int rendererIndex) throws ExoPlaybackException {
Renderer renderer = renderers[rendererIndex];
if (!isRendererEnabled(renderer)) {
return;
}
private void disableRenderer(int rendererIndex) {
int holderEnabledRendererCount = renderers[rendererIndex].getEnabledRendererCount();
renderers[rendererIndex].disable(mediaClock);
maybeTriggerOnRendererReadyChanged(rendererIndex, /* allowsPlayback= */ false);
mediaClock.onRendererDisabled(renderer);
ensureStopped(renderer);
renderer.disable();
enabledRendererCount--;
enabledRendererCount -= holderEnabledRendererCount;
}
private void reselectTracksInternalAndSeek() throws ExoPlaybackException {
@ -1925,16 +1897,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = isRendererEnabled(renderer);
SampleStream sampleStream = playingPeriodHolder.sampleStreams[i];
rendererWasEnabledFlags[i] = renderers[i].getEnabledRendererCount() > 0;
if (rendererWasEnabledFlags[i]) {
if (sampleStream != renderer.getStream()) {
// We need to disable the renderer.
disableRenderer(/* rendererIndex= */ i);
if (!renderers[i].isReadingFromPeriod(playingPeriodHolder)) {
disableRenderer(i);
} else if (streamResetFlags[i]) {
// The renderer will continue to consume from its current stream, but needs to be reset.
renderer.resetPosition(rendererPositionUs);
renderers[i].resetPosition(rendererPositionUs);
}
}
}
@ -2000,7 +1968,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
? livePlaybackSpeedControl.getTargetLiveOffsetUs()
: C.TIME_UNSET;
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod();
boolean isBufferedToEnd = loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal;
boolean isBufferedToEnd = (loadingHolder.isFullyBuffered() && loadingHolder.info.isFinal);
// Ad loader implementations may only load ad media once playback has nearly reached the ad, but
// it is possible for playback to be stuck buffering waiting for this. Therefore, we start
// playback regardless of buffered duration if we are waiting for an ad media period to prepare.
@ -2062,8 +2030,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* releaseMediaSourceList= */ false,
/* resetError= */ true);
}
for (Renderer renderer : renderers) {
renderer.setTimeline(timeline);
for (RendererHolder rendererHolder : renderers) {
rendererHolder.setTimeline(timeline);
}
if (!periodPositionChanged) {
// We can keep the current playing period. Update the rest of the queued periods.
@ -2181,12 +2149,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return maxReadPositionUs;
}
for (int i = 0; i < renderers.length; i++) {
if (!isRendererEnabled(renderers[i])
|| renderers[i].getStream() != readingHolder.sampleStreams[i]) {
if (!renderers[i].isReadingFromPeriod(readingHolder)) {
// Ignore disabled renderers and renderers with sample streams from previous periods.
continue;
}
long readingPositionUs = renderers[i].getReadingPositionUs();
long readingPositionUs = renderers[i].getReadingPositionUs(readingHolder);
if (readingPositionUs == C.TIME_END_OF_SOURCE) {
return C.TIME_END_OF_SOURCE;
} else {
@ -2250,19 +2217,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
// intentionally to pause at the end of the period.
if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) {
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
RendererHolder renderer = renderers[i];
if (!renderer.isReadingFromPeriod(readingPeriodHolder)) {
continue;
}
// Defer setting the stream as final until the renderer has actually consumed the whole
// stream in case of playlist changes that cause the stream to be no longer final.
if (sampleStream != null
&& renderer.getStream() == sampleStream
&& renderer.hasReadStreamToEnd()) {
if (renderer.hasReadStreamToEnd()) {
long streamEndPositionUs =
readingPeriodHolder.info.durationUs != C.TIME_UNSET
&& readingPeriodHolder.info.durationUs != C.TIME_END_OF_SOURCE
? readingPeriodHolder.getRendererOffset() + readingPeriodHolder.info.durationUs
: C.TIME_UNSET;
setCurrentStreamFinal(renderer, streamEndPositionUs);
renderer.setCurrentStreamFinal(streamEndPositionUs);
}
}
}
@ -2320,8 +2287,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// it's a no-sample renderer for which rendererOffsetUs should be updated only when
// starting to play the next period. Mark the SampleStream as final to play out any
// remaining data.
setCurrentStreamFinal(
renderers[i],
renderers[i].setCurrentStreamFinal(
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
}
}
@ -2384,12 +2350,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
boolean needsToWaitForRendererToEnd = false;
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
if (!isRendererEnabled(renderer)) {
RendererHolder renderer = renderers[i];
if (renderer.getEnabledRendererCount() == 0) {
continue;
}
boolean rendererIsReadingOldStream =
renderer.getStream() != readingPeriodHolder.sampleStreams[i];
boolean rendererIsReadingOldStream = !renderer.isReadingFromPeriod(readingPeriodHolder);
boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i);
if (rendererShouldBeEnabled && !rendererIsReadingOldStream) {
// All done.
@ -2411,7 +2376,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
} else if (renderer.isEnded()) {
// The renderer has finished playback, so we can disable it now.
disableRenderer(/* rendererIndex= */ i);
disableRenderer(i);
} else {
// We need to wait until rendering finished before disabling the renderer.
needsToWaitForRendererToEnd = true;
@ -2479,9 +2444,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void allowRenderersToRenderStartOfStreams() {
TrackSelectorResult playingTracks = queue.getPlayingPeriod().getTrackSelectorResult();
for (int i = 0; i < renderers.length; i++) {
if (playingTracks.isRendererEnabled(i)) {
renderers[i].enableMayRenderStartOfStream();
if (!playingTracks.isRendererEnabled(i)) {
continue;
}
renderers[i].enableMayRenderStartOfStream();
}
}
@ -2514,46 +2480,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
return false;
}
for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
if (renderer.getStream() != sampleStream
|| (sampleStream != null
&& !renderer.hasReadStreamToEnd()
&& !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder))) {
// The current reading period is still being read by at least one renderer.
if (!renderers[i].hasFinishedReadingFromPeriod(readingPeriodHolder)) {
return false;
}
}
return true;
}
private boolean hasReachedServerSideInsertedAdsTransition(
Renderer renderer, MediaPeriodHolder reading) {
MediaPeriodHolder nextPeriod = reading.getNext();
// We can advance the reading period early once we read beyond the transition point in a
// server-side inserted ads stream because we know the samples are read from the same underlying
// stream. This shortcut is helpful in case the transition point moved and renderers already
// read beyond the new transition point. But wait until the next period is actually prepared to
// allow a seamless transition.
return reading.info.isFollowedByTransitionToSameStream
&& nextPeriod.prepared
&& (renderer instanceof TextRenderer // [internal: b/181312195]
|| renderer instanceof MetadataRenderer
|| renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime());
}
private void setAllRendererStreamsFinal(long streamEndPositionUs) {
for (Renderer renderer : renderers) {
if (renderer.getStream() != null) {
setCurrentStreamFinal(renderer, streamEndPositionUs);
}
}
}
private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) {
renderer.setCurrentStreamFinal();
if (renderer instanceof TextRenderer) {
((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs);
for (RendererHolder renderer : renderers) {
renderer.setCurrentStreamFinal(streamEndPositionUs);
}
}
@ -2635,11 +2571,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
}
updateTrackSelectionPlaybackSpeed(playbackParameters.speed);
for (Renderer renderer : renderers) {
if (renderer != null) {
renderer.setPlaybackSpeed(
currentPlaybackSpeed, /* targetPlaybackSpeed= */ playbackParameters.speed);
}
for (RendererHolder rendererHolder : renderers) {
rendererHolder.setPlaybackSpeed(
currentPlaybackSpeed, /* targetPlaybackSpeed= */ playbackParameters.speed);
}
}
@ -2799,7 +2733,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Reset all disabled renderers before enabling any new ones. This makes sure resources released
// by the disabled renderers will be available to renderers that are being enabled.
for (int i = 0; i < renderers.length; i++) {
if (!trackSelectorResult.isRendererEnabled(i) && renderersToReset.remove(renderers[i])) {
if (!trackSelectorResult.isRendererEnabled(i)) {
renderers[i].reset();
}
}
@ -2814,11 +2748,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, long startPositionUs)
throws ExoPlaybackException {
Renderer renderer = renderers[rendererIndex];
if (isRendererEnabled(renderer)) {
MediaPeriodHolder periodHolder = queue.getReadingPeriod();
RendererHolder renderer = renderers[rendererIndex];
if (renderer.getEnabledRendererCount() > 0) {
return;
}
MediaPeriodHolder periodHolder = queue.getReadingPeriod();
boolean arePlayingAndReadingTheSamePeriod = periodHolder == queue.getPlayingPeriod();
TrackSelectorResult trackSelectorResult = periodHolder.getTrackSelectorResult();
RendererConfiguration rendererConfiguration =
@ -2831,7 +2765,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
boolean joining = !wasRendererEnabled && playing;
// Enable the renderer.
enabledRendererCount++;
renderersToReset.add(renderer);
renderer.enable(
rendererConfiguration,
formats,
@ -2841,7 +2774,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* mayRenderStartOfStream= */ arePlayingAndReadingTheSamePeriod,
startPositionUs,
periodHolder.getRendererOffset(),
periodHolder.info.id);
periodHolder.info.id,
mediaClock);
renderer.handleMessage(
Renderer.MSG_SET_WAKEUP_LISTENER,
new Renderer.WakeupListener() {
@ -2857,8 +2791,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
});
mediaClock.onRendererEnabled(renderer);
// Start the renderer if playing and the Playing and Reading periods are the same.
if (playing && arePlayingAndReadingTheSamePeriod) {
renderer.start();
@ -2948,7 +2880,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void maybeThrowRendererStreamError(int rendererIndex)
throws IOException, ExoPlaybackException {
Renderer renderer = renderers[rendererIndex];
RendererHolder renderer = renderers[rendererIndex];
try {
renderer.maybeThrowStreamError();
} catch (IOException | RuntimeException e) {
@ -3442,10 +3374,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
return formats;
}
private static boolean isRendererEnabled(Renderer renderer) {
return renderer.getState() != STATE_DISABLED;
}
private static final class SeekPosition {
public final Timeline timeline;

View File

@ -0,0 +1,446 @@
/*
* Copyright 2024 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;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
import static androidx.media3.exoplayer.Renderer.STATE_ENABLED;
import static androidx.media3.exoplayer.Renderer.STATE_STARTED;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.exoplayer.metadata.MetadataRenderer;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.exoplayer.text.TextRenderer;
import java.io.IOException;
/** Holds a {@link Renderer renderer}. */
/* package */ class RendererHolder {
private final Renderer renderer;
// Index of renderer in renderer list held by the {@link Player}.
private final int index;
private boolean requiresReset;
public RendererHolder(Renderer renderer, int index) {
this.renderer = renderer;
this.index = index;
requiresReset = false;
}
public int getEnabledRendererCount() {
return isRendererEnabled(renderer) ? 1 : 0;
}
/**
* Returns the track type that the renderer handles.
*
* @see Renderer#getTrackType()
*/
public @C.TrackType int getTrackType() {
return renderer.getTrackType();
}
/**
* Returns reading position from the {@link Renderer} enabled on the {@link MediaPeriodHolder
* media period}.
*
* <p>Call requires that {@link Renderer} is enabled on the provided {@link MediaPeriodHolder
* media period}.
*
* @param period The {@link MediaPeriodHolder media period}
* @return The {@link Renderer#getReadingPositionUs()} from the {@link Renderer} enabled on the
* {@link MediaPeriodHolder media period}.
*/
public long getReadingPositionUs(@Nullable MediaPeriodHolder period) {
Assertions.checkState(isReadingFromPeriod(period));
return renderer.getReadingPositionUs();
}
/**
* Invokes {@link Renderer#hasReadStreamToEnd()}.
*
* @see Renderer#hasReadStreamToEnd()
*/
public boolean hasReadStreamToEnd() {
return renderer.hasReadStreamToEnd();
}
/**
* Signals to the renderer that the current {@link SampleStream} will be the final one supplied
* before it is next disabled or reset.
*
* @see Renderer#setCurrentStreamFinal()
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
public void setCurrentStreamFinal(long streamEndPositionUs) {
setCurrentStreamFinal(renderer, streamEndPositionUs);
}
private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) {
renderer.setCurrentStreamFinal();
if (renderer instanceof TextRenderer) {
((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs);
}
}
/**
* Returns whether the current {@link SampleStream} will be the final one supplied before the
* renderer is next disabled or reset.
*
* @see Renderer#isCurrentStreamFinal()
*/
public boolean isCurrentStreamFinal() {
return renderer.isCurrentStreamFinal();
}
/**
* Invokes {@link Renderer#replaceStream}.
*
* @see Renderer#replaceStream
*/
public void replaceStream(
Format[] formats,
SampleStream stream,
long startPositionUs,
long offsetUs,
MediaSource.MediaPeriodId mediaPeriodId)
throws ExoPlaybackException {
renderer.replaceStream(formats, stream, startPositionUs, offsetUs, mediaPeriodId);
}
/**
* Returns minimum amount of playback clock time that must pass in order for the {@link #render}
* call to make progress.
*
* <p>Returns {@code Long.MAX_VALUE} if {@link Renderer renderers} are not enabled.
*
* @see Renderer#getDurationToProgressUs
* @param rendererPositionUs The current render position in microseconds, measured at the start of
* the current iteration of the rendering loop.
* @param rendererPositionElapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in
* microseconds, measured at the start of the current iteration of the rendering loop.
* @return Minimum amount of playback clock time that must pass before renderer is able to make
* progress.
*/
public long getMinDurationToProgressUs(
long rendererPositionUs, long rendererPositionElapsedRealtimeUs) {
return isRendererEnabled(renderer)
? renderer.getDurationToProgressUs(rendererPositionUs, rendererPositionElapsedRealtimeUs)
: Long.MAX_VALUE;
}
/**
* Calls {@link Renderer#enableMayRenderStartOfStream} on enabled {@link Renderer renderers}.
*
* @see Renderer#enableMayRenderStartOfStream
*/
public void enableMayRenderStartOfStream() {
if (isRendererEnabled(renderer)) {
renderer.enableMayRenderStartOfStream();
}
}
/**
* Calls {@link Renderer#setPlaybackSpeed} on the {@link Renderer renderers}.
*
* @see Renderer#setPlaybackSpeed
*/
public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed)
throws ExoPlaybackException {
renderer.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed);
}
/**
* Calls {@link Renderer#setTimeline} on the {@link Renderer renderers}.
*
* @see Renderer#setTimeline
*/
public void setTimeline(Timeline timeline) {
renderer.setTimeline(timeline);
}
/**
* Returns true if all renderers have {@link Renderer#isEnded() ended}.
*
* @see Renderer#isEnded()
* @return if all renderers have {@link Renderer#isEnded() ended}.
*/
public boolean isEnded() {
return renderer.isEnded();
}
/**
* Returns whether {@link Renderer} is enabled on a {@link MediaPeriodHolder media period}.
*
* @param period The {@link MediaPeriodHolder media period} to check.
* @return Whether {@link Renderer} is enabled on a {@link MediaPeriodHolder media period}.
*/
public boolean isReadingFromPeriod(@Nullable MediaPeriodHolder period) {
return getRendererReadingFromPeriod(period) != null;
}
/**
* Returns the {@link Renderer} that is enabled on the provided media {@link MediaPeriodHolder
* period}.
*
* <p>Returns null if the renderer is not enabled on the requested period.
*
* @param period The {@link MediaPeriodHolder period} with which to retrieve the linked {@link
* Renderer}
* @return {@link Renderer} enabled on the {@link MediaPeriodHolder period} or {@code null} if the
* renderer is not enabled on the provided period.
*/
@Nullable
private Renderer getRendererReadingFromPeriod(@Nullable MediaPeriodHolder period) {
if (period == null || period.sampleStreams[index] == null) {
return null;
}
if (renderer.getStream() == period.sampleStreams[index]) {
return renderer;
}
return null;
}
/**
* Returns whether the {@link Renderer renderers} are still reading a {@link MediaPeriodHolder
* media period}.
*
* @param periodHolder The {@link MediaPeriodHolder media period} to check.
* @return true if {@link Renderer renderers} are reading the current reading period.
*/
public boolean hasFinishedReadingFromPeriod(MediaPeriodHolder periodHolder) {
return hasFinishedReadingFromPeriodInternal(periodHolder);
}
private boolean hasFinishedReadingFromPeriodInternal(MediaPeriodHolder readingPeriodHolder) {
SampleStream sampleStream = readingPeriodHolder.sampleStreams[index];
if (renderer.getStream() != sampleStream
|| (sampleStream != null
&& !renderer.hasReadStreamToEnd()
&& !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder))) {
// The current reading period is still being read by at least one renderer.
return false;
}
return true;
}
private boolean hasReachedServerSideInsertedAdsTransition(
Renderer renderer, MediaPeriodHolder reading) {
MediaPeriodHolder nextPeriod = reading.getNext();
// We can advance the reading period early once we read beyond the transition point in a
// server-side inserted ads stream because we know the samples are read from the same underlying
// stream. This shortcut is helpful in case the transition point moved and renderers already
// read beyond the new transition point. But wait until the next period is actually prepared to
// allow a seamless transition.
return reading.info.isFollowedByTransitionToSameStream
&& nextPeriod != null
&& nextPeriod.prepared
&& (renderer instanceof TextRenderer // [internal: b/181312195]
|| renderer instanceof MetadataRenderer
|| renderer.getReadingPositionUs() >= nextPeriod.getStartPositionRendererTime());
}
/**
* Calls {@link Renderer#render} on all enabled {@link Renderer renderers}.
*
* @param rendererPositionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* @param rendererPositionElapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in
* microseconds, measured at the start of the current iteration of the rendering loop.
* @throws ExoPlaybackException If an error occurs.
*/
public void render(long rendererPositionUs, long rendererPositionElapsedRealtimeUs)
throws ExoPlaybackException {
if (isRendererEnabled(renderer)) {
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
}
}
/**
* Returns whether the renderers allow playback to continue.
*
* <p>Determine whether the renderer allows playback to continue. Playback can continue if the
* renderer is ready or ended. Also continue playback if the renderer is reading ahead into the
* next stream or is waiting for the next stream. This is to avoid getting stuck if tracks in the
* current period have uneven durations and are still being read by another renderer. See:
* https://github.com/google/ExoPlayer/issues/1874.
*
* @param playingPeriodHolder The currently playing media {@link MediaPeriodHolder period}.
* @return whether renderer allows playback.
*/
public boolean allowsPlayback(MediaPeriodHolder playingPeriodHolder) throws IOException {
return allowsPlayback(renderer, playingPeriodHolder);
}
private boolean allowsPlayback(Renderer renderer, MediaPeriodHolder playingPeriodHolder) {
boolean isReadingAhead = playingPeriodHolder.sampleStreams[index] != renderer.getStream();
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
return isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
}
/**
* Invokes {Renderer#maybeThrowStreamError}.
*
* @see Renderer#maybeThrowStreamError()
*/
public void maybeThrowStreamError() throws IOException {
renderer.maybeThrowStreamError();
}
/**
* Calls {@link Renderer#start()} on all enabled {@link Renderer renderers}.
*
* @throws ExoPlaybackException If an error occurs.
*/
public void start() throws ExoPlaybackException {
if (renderer.getState() == STATE_ENABLED) {
renderer.start();
}
}
/** Calls {@link Renderer#stop()} on all enabled {@link Renderer renderers}. */
public void stop() {
if (isRendererEnabled(renderer)) {
ensureStopped(renderer);
}
}
private void ensureStopped(Renderer renderer) {
if (renderer.getState() == STATE_STARTED) {
renderer.stop();
}
}
/**
* Enables the renderer to consume from the specified {@link SampleStream}.
*
* @see Renderer#enable
* @param configuration The renderer configuration.
* @param formats The enabled formats.
* @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @param mayRenderStartOfStream Whether this renderer is allowed to render the start of the
* stream even if the state is not {@link Renderer#STATE_STARTED} yet.
* @param startPositionUs The start position of the stream in renderer time (microseconds).
* @param offsetUs The offset to be added to timestamps of buffers read from {@code stream} before
* they are rendered.
* @param mediaPeriodId The {@link MediaSource.MediaPeriodId} of the {@link MediaPeriod} producing
* the {@code stream}.
* @param mediaClock The {@link DefaultMediaClock} with which to call {@link
* DefaultMediaClock#onRendererEnabled(Renderer)}.
* @throws ExoPlaybackException If an error occurs.
*/
public void enable(
RendererConfiguration configuration,
Format[] formats,
SampleStream stream,
long positionUs,
boolean joining,
boolean mayRenderStartOfStream,
long startPositionUs,
long offsetUs,
MediaSource.MediaPeriodId mediaPeriodId,
DefaultMediaClock mediaClock)
throws ExoPlaybackException {
requiresReset = true;
renderer.enable(
configuration,
formats,
stream,
positionUs,
joining,
mayRenderStartOfStream,
startPositionUs,
offsetUs,
mediaPeriodId);
mediaClock.onRendererEnabled(renderer);
}
/**
* Invokes {@link Renderer#handleMessage} on the {@link Renderer}.
*
* @see Renderer#handleMessage(int, Object)
*/
public void handleMessage(@Renderer.MessageType int messageType, @Nullable Object message)
throws ExoPlaybackException {
renderer.handleMessage(messageType, message);
}
/**
* Stops and disables all {@link Renderer renderers}.
*
* @param mediaClock To call {@link DefaultMediaClock#onRendererDisabled} if disabling a {@link
* Renderer}.
*/
public void disable(DefaultMediaClock mediaClock) {
disableRenderer(renderer, mediaClock);
}
/**
* Disable a {@link Renderer} if its enabled.
*
* <p>The {@link DefaultMediaClock#onRendererDisabled} callback will be invoked if the renderer is
* disabled.
*
* @param renderer The {@link Renderer} to disable.
* @param mediaClock The {@link DefaultMediaClock} to invoke {@link
* DefaultMediaClock#onRendererDisabled onRendererDisabled} with the provided {@code
* renderer}.
*/
private void disableRenderer(Renderer renderer, DefaultMediaClock mediaClock) {
if (!isRendererEnabled(renderer)) {
return;
}
mediaClock.onRendererDisabled(renderer);
ensureStopped(renderer);
renderer.disable();
}
/**
* Calls {@link Renderer#resetPosition} on the {@link Renderer} if its enabled.
*
* @see Renderer#resetPosition
*/
public void resetPosition(long positionUs) throws ExoPlaybackException {
if (isRendererEnabled(renderer)) {
renderer.resetPosition(positionUs);
}
}
/** Calls {@link Renderer#reset()} on all renderers that must be reset. */
public void reset() {
if (requiresReset) {
renderer.reset();
requiresReset = false;
}
}
/** Calls {@link Renderer#release()} on all {@link Renderer renderers}. */
public void release() {
renderer.release();
requiresReset = false;
}
private static boolean isRendererEnabled(Renderer renderer) {
return renderer.getState() != STATE_DISABLED;
}
}