Implement secondary renderer feature for pre-warming

`RenderersFactory#createSecondaryRenderer` can be implemented to provide secondary renderers for pre-warming. These renderers must match their primaries in terms of reported track type support and `RendererCapabilities`.

If a secondary renderer is provided, ExoPlayer will enable it for a subsequent media item as soon as its `MediaPeriod` is prepared. This will cause the renderer to start decoding and processing content so that it is ready to play as soon as playback transitions to that media item.

PiperOrigin-RevId: 704326302
This commit is contained in:
michaelkatz 2024-12-09 10:04:30 -08:00 committed by Copybara-Service
parent fc3f096888
commit 987869e456
15 changed files with 5515 additions and 272 deletions

View File

@ -24,6 +24,9 @@
functionality to platform `MediaExtractor`. functionality to platform `MediaExtractor`.
* Move `BasePreloadManager.Listener` to a top level * Move `BasePreloadManager.Listener` to a top level
`PreloadManagerListener`. `PreloadManagerListener`.
* `RenderersFactory.createSecondaryRenderer` can be implemented to provide
secondary renderers for pre-warming. Pre-warming enables quicker media
item transitions during playback.
* Transformer: * Transformer:
* Update parameters of `VideoFrameProcessor.registerInputStream` and * Update parameters of `VideoFrameProcessor.registerInputStream` and
`VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`. `VideoFrameProcessor.Listener.onInputStreamRegistered` to use `Format`.

View File

@ -1481,6 +1481,19 @@ public interface ExoPlayer extends Player {
@UnstableApi @UnstableApi
Renderer getRenderer(int index); Renderer getRenderer(int index);
/**
* Returns the secondary renderer at the given index.
*
* @param index The index of the secondary renderer.
* @return The secondary renderer at this index, or null if there is no secondary renderer at this
* index.
*/
@UnstableApi
@Nullable
default Renderer getSecondaryRenderer(int index) {
return null;
}
/** /**
* Returns the track selector that this player uses, or null if track selection is not supported. * Returns the track selector that this player uses, or null if track selection is not supported.
*/ */

View File

@ -87,6 +87,7 @@ import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.NullableType;
import androidx.media3.common.util.Size; import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.PlayerMessage.Target; import androidx.media3.exoplayer.PlayerMessage.Target;
@ -153,6 +154,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
private final Context applicationContext; private final Context applicationContext;
private final Player wrappingPlayer; private final Player wrappingPlayer;
private final Renderer[] renderers; private final Renderer[] renderers;
private final @NullableType Renderer[] secondaryRenderers;
private final TrackSelector trackSelector; private final TrackSelector trackSelector;
private final HandlerWrapper playbackInfoUpdateHandler; private final HandlerWrapper playbackInfoUpdateHandler;
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener; private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
@ -263,17 +265,27 @@ import java.util.concurrent.CopyOnWriteArraySet;
componentListener = new ComponentListener(); componentListener = new ComponentListener();
frameMetadataListener = new FrameMetadataListener(); frameMetadataListener = new FrameMetadataListener();
Handler eventHandler = new Handler(builder.looper); Handler eventHandler = new Handler(builder.looper);
RenderersFactory renderersFactory = builder.renderersFactorySupplier.get();
renderers = renderers =
builder renderersFactory.createRenderers(
.renderersFactorySupplier eventHandler,
.get() componentListener,
.createRenderers( componentListener,
eventHandler, componentListener,
componentListener, componentListener);
componentListener,
componentListener,
componentListener);
checkState(renderers.length > 0); checkState(renderers.length > 0);
secondaryRenderers = new Renderer[renderers.length];
for (int i = 0; i < secondaryRenderers.length; i++) {
// TODO(b/377671489): Fix DefaultAnalyticsCollector logic to still work with pre-warming.
secondaryRenderers[i] =
renderersFactory.createSecondaryRenderer(
renderers[i],
eventHandler,
componentListener,
componentListener,
componentListener,
componentListener);
}
this.trackSelector = builder.trackSelectorSupplier.get(); this.trackSelector = builder.trackSelectorSupplier.get();
this.mediaSourceFactory = builder.mediaSourceFactorySupplier.get(); this.mediaSourceFactory = builder.mediaSourceFactorySupplier.get();
this.bandwidthMeter = builder.bandwidthMeterSupplier.get(); this.bandwidthMeter = builder.bandwidthMeterSupplier.get();
@ -357,6 +369,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
internalPlayer = internalPlayer =
new ExoPlayerImplInternal( new ExoPlayerImplInternal(
renderers, renderers,
secondaryRenderers,
trackSelector, trackSelector,
emptyTrackSelectorResult, emptyTrackSelectorResult,
builder.loadControlSupplier.get(), builder.loadControlSupplier.get(),
@ -1225,6 +1238,13 @@ import java.util.concurrent.CopyOnWriteArraySet;
return renderers[index]; return renderers[index];
} }
@Override
@Nullable
public Renderer getSecondaryRenderer(int index) {
verifyApplicationThread();
return secondaryRenderers[index];
}
@Override @Override
public TrackSelector getTrackSelector() { public TrackSelector getTrackSelector() {
verifyApplicationThread(); verifyApplicationThread();

View File

@ -19,6 +19,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.msToUs; import static androidx.media3.common.util.Util.msToUs;
import static androidx.media3.exoplayer.MediaPeriodQueue.REMOVE_AFTER_REMOVED_READING_PERIOD;
import static androidx.media3.exoplayer.RendererHolder.REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED;
import static androidx.media3.exoplayer.RendererHolder.REPLACE_STREAMS_DISABLE_RENDERERS_DISABLE_OFFLOAD_SCHEDULING;
import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED; import static androidx.media3.exoplayer.audio.AudioSink.OFFLOAD_MODE_DISABLED;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -35,6 +38,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.IllegalSeekPositionException; import androidx.media3.common.IllegalSeekPositionException;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.ParserException; import androidx.media3.common.ParserException;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackException.ErrorCode; import androidx.media3.common.PlaybackException.ErrorCode;
@ -200,6 +204,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final boolean dynamicSchedulingEnabled; private final boolean dynamicSchedulingEnabled;
private final AnalyticsCollector analyticsCollector; private final AnalyticsCollector analyticsCollector;
private final HandlerWrapper applicationLooperHandler; private final HandlerWrapper applicationLooperHandler;
private final boolean hasSecondaryRenderers;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private SeekParameters seekParameters; private SeekParameters seekParameters;
@ -228,9 +233,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
private long playbackMaybeBecameStuckAtMs; private long playbackMaybeBecameStuckAtMs;
private PreloadConfiguration preloadConfiguration; private PreloadConfiguration preloadConfiguration;
private Timeline lastPreloadPoolInvalidationTimeline; private Timeline lastPreloadPoolInvalidationTimeline;
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
public ExoPlayerImplInternal( public ExoPlayerImplInternal(
Renderer[] renderers, Renderer[] renderers,
Renderer[] secondaryRenderers,
TrackSelector trackSelector, TrackSelector trackSelector,
TrackSelectorResult emptyTrackSelectorResult, TrackSelectorResult emptyTrackSelectorResult,
LoadControl loadControl, LoadControl loadControl,
@ -281,6 +288,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
RendererCapabilities.Listener rendererCapabilitiesListener = RendererCapabilities.Listener rendererCapabilitiesListener =
trackSelector.getRendererCapabilitiesListener(); trackSelector.getRendererCapabilitiesListener();
boolean hasSecondaryRenderers = false;
this.renderers = new RendererHolder[renderers.length]; this.renderers = new RendererHolder[renderers.length];
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
renderers[i].init(/* index= */ i, playerId, clock); renderers[i].init(/* index= */ i, playerId, clock);
@ -288,8 +296,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (rendererCapabilitiesListener != null) { if (rendererCapabilitiesListener != null) {
rendererCapabilities[i].setListener(rendererCapabilitiesListener); rendererCapabilities[i].setListener(rendererCapabilitiesListener);
} }
this.renderers[i] = new RendererHolder(renderers[i], /* index= */ i); if (secondaryRenderers[i] != null) {
secondaryRenderers[i].init(/* index= */ i, playerId, clock);
hasSecondaryRenderers = true;
}
this.renderers[i] = new RendererHolder(renderers[i], secondaryRenderers[i], /* index= */ i);
} }
this.hasSecondaryRenderers = hasSecondaryRenderers;
mediaClock = new DefaultMediaClock(this, clock); mediaClock = new DefaultMediaClock(this, clock);
pendingMessages = new ArrayList<>(); pendingMessages = new ArrayList<>();
window = new Timeline.Window(); window = new Timeline.Window();
@ -1477,6 +1491,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
newPlayingPeriodHolder.setRendererOffset( newPlayingPeriodHolder.setRendererOffset(
MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US); MediaPeriodQueue.INITIAL_RENDERER_POSITION_OFFSET_US);
enableRenderers(); enableRenderers();
newPlayingPeriodHolder.allRenderersInCorrectState = true;
} }
} }
@ -1512,7 +1527,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
: playingMediaPeriod.toRendererTime(periodPositionUs); : playingMediaPeriod.toRendererTime(periodPositionUs);
mediaClock.resetPosition(rendererPositionUs); mediaClock.resetPosition(rendererPositionUs);
for (RendererHolder rendererHolder : renderers) { for (RendererHolder rendererHolder : renderers) {
rendererHolder.resetPosition(rendererPositionUs); rendererHolder.resetPosition(playingMediaPeriod, rendererPositionUs);
} }
notifyTrackSelectionDiscontinuity(); notifyTrackSelectionDiscontinuity();
} }
@ -1533,9 +1548,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
this.foregroundMode = foregroundMode; this.foregroundMode = foregroundMode;
if (!foregroundMode) { if (!foregroundMode) {
for (RendererHolder rendererHolder : renderers) { for (RendererHolder rendererHolder : renderers) {
if (rendererHolder.getEnabledRendererCount() == 0) { rendererHolder.reset();
rendererHolder.reset();
}
} }
} }
} }
@ -1873,15 +1886,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
private void disableRenderers() { private void disableRenderers() {
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
disableRenderer(i); disableRenderer(/* rendererIndex= */ i);
} }
prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
} }
private void disableRenderer(int rendererIndex) { private void disableRenderer(int rendererIndex) {
int holderEnabledRendererCount = renderers[rendererIndex].getEnabledRendererCount(); int enabledRendererCountBeforeDisabling = renderers[rendererIndex].getEnabledRendererCount();
renderers[rendererIndex].disable(mediaClock); renderers[rendererIndex].disable(mediaClock);
maybeTriggerOnRendererReadyChanged(rendererIndex, /* allowsPlayback= */ false); maybeTriggerOnRendererReadyChanged(rendererIndex, /* allowsPlayback= */ false);
enabledRendererCount -= holderEnabledRendererCount; enabledRendererCount -= enabledRendererCountBeforeDisabling;
} }
private void reselectTracksInternalAndSeek() throws ExoPlaybackException { private void reselectTracksInternalAndSeek() throws ExoPlaybackException {
@ -1923,7 +1937,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (selectionsChangedForReadPeriod) { if (selectionsChangedForReadPeriod) {
// Update streams and rebuffer for the new selection, recreating all streams if reading ahead. // Update streams and rebuffer for the new selection, recreating all streams if reading ahead.
MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod();
boolean recreateStreams = queue.removeAfter(playingPeriodHolder); int removeAfterResult = queue.removeAfter(playingPeriodHolder);
boolean recreateStreams = (removeAfterResult & REMOVE_AFTER_REMOVED_READING_PERIOD) != 0;
boolean[] streamResetFlags = new boolean[renderers.length]; boolean[] streamResetFlags = new boolean[renderers.length];
long periodPositionUs = long periodPositionUs =
@ -1954,7 +1969,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (!renderers[i].isReadingFromPeriod(playingPeriodHolder)) { if (!renderers[i].isReadingFromPeriod(playingPeriodHolder)) {
disableRenderer(i); disableRenderer(i);
} else if (streamResetFlags[i]) { } else if (streamResetFlags[i]) {
renderers[i].resetPosition(rendererPositionUs); renderers[i].resetPosition(playingPeriodHolder, rendererPositionUs);
} }
} }
} }
@ -2020,7 +2035,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
? livePlaybackSpeedControl.getTargetLiveOffsetUs() ? livePlaybackSpeedControl.getTargetLiveOffsetUs()
: C.TIME_UNSET; : C.TIME_UNSET;
MediaPeriodHolder loadingHolder = queue.getLoadingPeriod(); 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 // 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 // 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. // playback regardless of buffered duration if we are waiting for an ad media period to prepare.
@ -2221,6 +2236,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
return; return;
} }
boolean loadingPeriodChanged = maybeUpdateLoadingPeriod(); boolean loadingPeriodChanged = maybeUpdateLoadingPeriod();
maybeUpdatePrewarmingPeriod();
maybeUpdateReadingPeriod(); maybeUpdateReadingPeriod();
maybeUpdateReadingRenderers(); maybeUpdateReadingRenderers();
maybeUpdatePlayingPeriod(); maybeUpdatePlayingPeriod();
@ -2258,6 +2274,54 @@ import java.util.concurrent.atomic.AtomicBoolean;
return loadingPeriodChanged; return loadingPeriodChanged;
} }
private void maybeUpdatePrewarmingPeriod() throws ExoPlaybackException {
// TODO: Add limit as to not enable waiting renderer too early
if (pendingPauseAtEndOfPeriod || !hasSecondaryRenderers || areRenderersPrewarming()) {
return;
}
@Nullable MediaPeriodHolder prewarmingPeriodHolder = queue.getPrewarmingPeriod();
if (prewarmingPeriodHolder == null
|| prewarmingPeriodHolder != queue.getReadingPeriod()
|| prewarmingPeriodHolder.getNext() == null
|| !prewarmingPeriodHolder.getNext().prepared) {
return;
}
queue.advancePrewarmingPeriod();
maybePrewarmRenderers();
}
private void maybePrewarmRenderers() throws ExoPlaybackException {
@Nullable MediaPeriodHolder prewarmingPeriod = queue.getPrewarmingPeriod();
if (prewarmingPeriod == null) {
return;
}
TrackSelectorResult trackSelectorResult = prewarmingPeriod.getTrackSelectorResult();
for (int i = 0; i < renderers.length; i++) {
if (trackSelectorResult.isRendererEnabled(i)
&& renderers[i].hasSecondary()
&& !renderers[i].isPrewarming()) {
renderers[i].startPrewarming();
enableRenderer(
prewarmingPeriod,
/* rendererIndex= */ i,
/* wasRendererEnabled= */ false,
prewarmingPeriod.getStartPositionRendererTime());
}
}
// Handle any media period discontinuities.
if (areRenderersPrewarming()) {
prewarmingMediaPeriodDiscontinuity = prewarmingPeriod.mediaPeriod.readDiscontinuity();
if (!prewarmingPeriod.isFullyBuffered()) {
// The discontinuity caused the period to not be fully buffered. Continue loading from
// this period again and discard all other periods we already started loading.
queue.removeAfter(prewarmingPeriod);
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
maybeContinueLoading();
}
}
}
private void maybeUpdateReadingPeriod() throws ExoPlaybackException { private void maybeUpdateReadingPeriod() throws ExoPlaybackException {
@Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); @Nullable MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
if (readingPeriodHolder == null) { if (readingPeriodHolder == null) {
@ -2268,20 +2332,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
// We don't have a successor to advance the reading period to or we want to let them end // We don't have a successor to advance the reading period to or we want to let them end
// intentionally to pause at the end of the period. // intentionally to pause at the end of the period.
if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) { if (readingPeriodHolder.info.isFinal || pendingPauseAtEndOfPeriod) {
for (int i = 0; i < renderers.length; i++) { for (RendererHolder renderer : renderers) {
RendererHolder renderer = renderers[i];
if (!renderer.isReadingFromPeriod(readingPeriodHolder)) { if (!renderer.isReadingFromPeriod(readingPeriodHolder)) {
continue; continue;
} }
// Defer setting the stream as final until the renderer has actually consumed the whole // 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. // stream in case of playlist changes that cause the stream to be no longer final.
if (renderer.hasReadStreamToEnd()) { if (renderer.hasReadPeriodToEnd(readingPeriodHolder)) {
long streamEndPositionUs = long streamEndPositionUs =
readingPeriodHolder.info.durationUs != C.TIME_UNSET readingPeriodHolder.info.durationUs != C.TIME_UNSET
&& readingPeriodHolder.info.durationUs != C.TIME_END_OF_SOURCE && readingPeriodHolder.info.durationUs != C.TIME_END_OF_SOURCE
? readingPeriodHolder.getRendererOffset() + readingPeriodHolder.info.durationUs ? readingPeriodHolder.getRendererOffset() + readingPeriodHolder.info.durationUs
: C.TIME_UNSET; : C.TIME_UNSET;
renderer.setCurrentStreamFinal(streamEndPositionUs); renderer.setCurrentStreamFinal(readingPeriodHolder, streamEndPositionUs);
} }
} }
} }
@ -2292,6 +2355,11 @@ import java.util.concurrent.atomic.AtomicBoolean;
return; return;
} }
if (areRenderersPrewarming() && queue.getPrewarmingPeriod() == queue.getReadingPeriod()) {
// Reading period has already advanced to pre-warming period.
return;
}
if (!readingPeriodHolder.getNext().prepared if (!readingPeriodHolder.getNext().prepared
&& rendererPositionUs < readingPeriodHolder.getNext().getStartPositionRendererTime()) { && rendererPositionUs < readingPeriodHolder.getNext().getStartPositionRendererTime()) {
// The successor is not prepared yet and playback hasn't reached the transition point. // The successor is not prepared yet and playback hasn't reached the transition point.
@ -2312,37 +2380,48 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* forceSetTargetOffsetOverride= */ false); /* forceSetTargetOffsetOverride= */ false);
if (readingPeriodHolder.prepared if (readingPeriodHolder.prepared
&& readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { && ((hasSecondaryRenderers && prewarmingMediaPeriodDiscontinuity != C.TIME_UNSET)
// The new period starts with a discontinuity, so the renderers will play out all data, then || readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET)) {
prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
// The new period starts with a discontinuity, so unless a pre-warming renderer is handling
// the discontinuity, the renderers will play out all data, then
// be disabled and re-enabled when they start playing the next period. // be disabled and re-enabled when they start playing the next period.
setAllRendererStreamsFinal( boolean arePrewarmingRenderersHandlingDiscontinuity = hasSecondaryRenderers;
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime()); if (arePrewarmingRenderersHandlingDiscontinuity) {
if (!readingPeriodHolder.isFullyBuffered()) { for (int i = 0; i < renderers.length; i++) {
// The discontinuity caused the period to not be fully buffered. Continue loading from this if (!newTrackSelectorResult.isRendererEnabled(i)) {
// period again and discard all other periods we already started loading. continue;
queue.removeAfter(readingPeriodHolder); }
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); // TODO: This check should ideally be replaced by a per-stream discontinuity check
maybeContinueLoading(); // done by the MediaPeriod itself.
} if (!MimeTypes.allSamplesAreSyncSamples(
return; newTrackSelectorResult.selections[i].getSelectedFormat().sampleMimeType,
} newTrackSelectorResult.selections[i].getSelectedFormat().codecs)
for (int i = 0; i < renderers.length; i++) { && !renderers[i].isPrewarming()) {
boolean oldRendererEnabled = oldTrackSelectorResult.isRendererEnabled(i); arePrewarmingRenderersHandlingDiscontinuity = false;
boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(i); break;
if (oldRendererEnabled && !renderers[i].isCurrentStreamFinal()) { }
boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE;
RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
if (!newRendererEnabled || !newConfig.equals(oldConfig) || isNoSampleRenderer) {
// The renderer will be disabled when transitioning to playing the next period, because
// there's no new selection, or because a configuration change is required, or because
// 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.
renderers[i].setCurrentStreamFinal(
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
} }
} }
if (!arePrewarmingRenderersHandlingDiscontinuity) {
setAllNonPrewarmingRendererStreamsFinal(
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
if (!readingPeriodHolder.isFullyBuffered()) {
// The discontinuity caused the period to not be fully buffered. Continue loading from
// this period again and discard all other periods we already started loading.
queue.removeAfter(readingPeriodHolder);
handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false);
maybeContinueLoading();
}
return;
}
}
for (RendererHolder renderer : renderers) {
renderer.maybeSetOldStreamToFinal(
oldTrackSelectorResult,
newTrackSelectorResult,
readingPeriodHolder.getStartPositionRendererTime());
} }
} }
@ -2354,11 +2433,48 @@ import java.util.concurrent.atomic.AtomicBoolean;
// Not reading ahead or all renderers updated. // Not reading ahead or all renderers updated.
return; return;
} }
if (replaceStreamsOrDisableRendererForTransition()) { boolean allUpdated = updateRenderersForTransition();
enableRenderers(); if (allUpdated) {
queue.getReadingPeriod().allRenderersInCorrectState = true;
} }
} }
private boolean updateRenderersForTransition() throws ExoPlaybackException {
MediaPeriodHolder readingMediaPeriod = queue.getReadingPeriod();
TrackSelectorResult newTrackSelectorResult = readingMediaPeriod.getTrackSelectorResult();
boolean allUpdated = true;
for (int i = 0; i < renderers.length; i++) {
int enabledRendererCountPreTransition = renderers[i].getEnabledRendererCount();
int result =
renderers[i].replaceStreamsOrDisableRendererForTransition(
readingMediaPeriod, newTrackSelectorResult, mediaClock);
if ((result & REPLACE_STREAMS_DISABLE_RENDERERS_DISABLE_OFFLOAD_SCHEDULING) != 0
&& offloadSchedulingEnabled) {
// Prevent sleeping across offload track transition else position won't get updated.
// TODO: (b/183635183) Optimize Offload End-Of-Stream: Sleep to just before end of track
setOffloadSchedulingEnabled(false);
}
enabledRendererCount -=
enabledRendererCountPreTransition - renderers[i].getEnabledRendererCount();
boolean completedUpdate = (result & REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED) != 0;
allUpdated &= completedUpdate;
}
if (allUpdated) {
for (int i = 0; i < renderers.length; i++) {
if (newTrackSelectorResult.isRendererEnabled(i)
&& !renderers[i].isReadingFromPeriod(readingMediaPeriod)) {
enableRenderer(
readingMediaPeriod,
/* rendererIndex= */ i,
/* wasRendererEnabled= */ false,
readingMediaPeriod.getStartPositionRendererTime());
}
}
}
return allUpdated;
}
private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) { private void maybeUpdatePreloadPeriods(boolean loadingPeriodChanged) {
if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET) { if (preloadConfiguration.targetPreloadDurationUs == C.TIME_UNSET) {
// Do nothing if preloading disabled. // Do nothing if preloading disabled.
@ -2397,46 +2513,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private boolean replaceStreamsOrDisableRendererForTransition() throws ExoPlaybackException {
MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod();
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
boolean needsToWaitForRendererToEnd = false;
for (int i = 0; i < renderers.length; i++) {
RendererHolder renderer = renderers[i];
if (renderer.getEnabledRendererCount() == 0) {
continue;
}
boolean rendererIsReadingOldStream = !renderer.isReadingFromPeriod(readingPeriodHolder);
boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(i);
if (rendererShouldBeEnabled && !rendererIsReadingOldStream) {
// All done.
continue;
}
if (!renderer.isCurrentStreamFinal()) {
// The renderer stream is not final, so we can replace the sample streams immediately.
Format[] formats = getFormats(newTrackSelectorResult.selections[i]);
renderer.replaceStream(
formats,
readingPeriodHolder.sampleStreams[i],
readingPeriodHolder.getStartPositionRendererTime(),
readingPeriodHolder.getRendererOffset(),
readingPeriodHolder.info.id);
if (offloadSchedulingEnabled) {
// Prevent sleeping across offload track transition else position won't get updated.
// TODO: (b/183635183) Optimize Offload End-Of-Stream: Sleep to just before end of track
setOffloadSchedulingEnabled(false);
}
} else if (renderer.isEnded()) {
// The renderer has finished playback, so we can disable it now.
disableRenderer(i);
} else {
// We need to wait until rendering finished before disabling the renderer.
needsToWaitForRendererToEnd = true;
}
}
return !needsToWaitForRendererToEnd;
}
private void maybeUpdatePlayingPeriod() throws ExoPlaybackException { private void maybeUpdatePlayingPeriod() throws ExoPlaybackException {
boolean advancedPlayingPeriod = false; boolean advancedPlayingPeriod = false;
while (shouldAdvancePlayingPeriod()) { while (shouldAdvancePlayingPeriod()) {
@ -2461,6 +2537,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
Player.DISCONTINUITY_REASON_AUTO_TRANSITION); Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
resetPendingPauseAtEndOfPeriod(); resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions(); updatePlaybackPositions();
if (areRenderersPrewarming() && newPlayingPeriodHolder == queue.getPrewarmingPeriod()) {
maybeHandlePrewarmingTransition();
}
if (playbackInfo.playbackState == Player.STATE_READY) { if (playbackInfo.playbackState == Player.STATE_READY) {
startRenderers(); startRenderers();
} }
@ -2469,6 +2548,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private void maybeHandlePrewarmingTransition() {
for (RendererHolder renderer : renderers) {
renderer.maybeHandlePrewarmingTransition();
}
}
private void maybeUpdateOffloadScheduling() { private void maybeUpdateOffloadScheduling() {
// If playing period is audio-only with offload mode preference to enable, then offload // If playing period is audio-only with offload mode preference to enable, then offload
// scheduling should be enabled. // scheduling should be enabled.
@ -2539,9 +2624,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
return true; return true;
} }
private void setAllRendererStreamsFinal(long streamEndPositionUs) { private void setAllNonPrewarmingRendererStreamsFinal(long streamEndPositionUs) {
for (RendererHolder renderer : renderers) { for (RendererHolder renderer : renderers) {
renderer.setCurrentStreamFinal(streamEndPositionUs); renderer.setAllNonPrewarmingRendererStreamsFinal(streamEndPositionUs);
} }
} }
@ -2579,6 +2664,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
// This is the first prepared period, so update the position and the renderers. // This is the first prepared period, so update the position and the renderers.
resetRendererPosition(loadingPeriodHolder.info.startPositionUs); resetRendererPosition(loadingPeriodHolder.info.startPositionUs);
enableRenderers(); enableRenderers();
loadingPeriodHolder.allRenderersInCorrectState = true;
playbackInfo = playbackInfo =
handlePositionDiscontinuity( handlePositionDiscontinuity(
playbackInfo.periodId, playbackInfo.periodId,
@ -2789,20 +2875,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
renderers[i].reset(); renderers[i].reset();
} }
} }
// Enable the renderers.
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
if (trackSelectorResult.isRendererEnabled(i)) { if (trackSelectorResult.isRendererEnabled(i)
enableRenderer(i, rendererWasEnabledFlags[i], startPositionUs); && !renderers[i].isReadingFromPeriod(readingMediaPeriod)) {
enableRenderer(
readingMediaPeriod,
/* rendererIndex= */ i,
rendererWasEnabledFlags[i],
startPositionUs);
} }
} }
readingMediaPeriod.allRenderersInCorrectState = true;
} }
private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, long startPositionUs) private void enableRenderer(
MediaPeriodHolder periodHolder,
int rendererIndex,
boolean wasRendererEnabled,
long startPositionUs)
throws ExoPlaybackException { throws ExoPlaybackException {
MediaPeriodHolder periodHolder = queue.getReadingPeriod();
RendererHolder renderer = renderers[rendererIndex]; RendererHolder renderer = renderers[rendererIndex];
if (renderer.getEnabledRendererCount() > 0) { if (renderer.isRendererEnabled()) {
return; return;
} }
boolean arePlayingAndReadingTheSamePeriod = periodHolder == queue.getPlayingPeriod(); boolean arePlayingAndReadingTheSamePeriod = periodHolder == queue.getPlayingPeriod();
@ -2810,16 +2902,16 @@ import java.util.concurrent.atomic.AtomicBoolean;
RendererConfiguration rendererConfiguration = RendererConfiguration rendererConfiguration =
trackSelectorResult.rendererConfigurations[rendererIndex]; trackSelectorResult.rendererConfigurations[rendererIndex];
ExoTrackSelection newSelection = trackSelectorResult.selections[rendererIndex]; ExoTrackSelection newSelection = trackSelectorResult.selections[rendererIndex];
Format[] formats = getFormats(newSelection);
// The renderer needs enabling with its new track selection. // The renderer needs enabling with its new track selection.
boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY; boolean playing = shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY;
// Consider as joining only if the renderer was previously disabled. // Consider as joining only if the renderer was previously disabled and being enabled on the
// playing period.
boolean joining = !wasRendererEnabled && playing; boolean joining = !wasRendererEnabled && playing;
// Enable the renderer. // Enable the renderer.
enabledRendererCount++; enabledRendererCount++;
renderer.enable( renderer.enable(
rendererConfiguration, rendererConfiguration,
formats, newSelection,
periodHolder.sampleStreams[rendererIndex], periodHolder.sampleStreams[rendererIndex],
rendererPositionUs, rendererPositionUs,
joining, joining,
@ -2842,7 +2934,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
handler.sendEmptyMessage(MSG_DO_SOME_WORK); handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} }
} }
}); },
/* mediaPeriod= */ periodHolder);
// Start the renderer if playing and the Playing and Reading periods are the same. // Start the renderer if playing and the Playing and Reading periods are the same.
if (playing && arePlayingAndReadingTheSamePeriod) { if (playing && arePlayingAndReadingTheSamePeriod) {
renderer.start(); renderer.start();
@ -2934,7 +3027,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
throws IOException, ExoPlaybackException { throws IOException, ExoPlaybackException {
RendererHolder renderer = renderers[rendererIndex]; RendererHolder renderer = renderers[rendererIndex];
try { try {
renderer.maybeThrowStreamError(); renderer.maybeThrowStreamError(checkNotNull(queue.getPlayingPeriod()));
} catch (IOException | RuntimeException e) { } catch (IOException | RuntimeException e) {
switch (renderer.getTrackType()) { switch (renderer.getTrackType()) {
case C.TRACK_TYPE_TEXT: case C.TRACK_TYPE_TEXT:
@ -2970,6 +3063,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
} }
} }
private boolean areRenderersPrewarming() {
if (!hasSecondaryRenderers) {
return false;
}
for (RendererHolder renderer : renderers) {
if (renderer.isPrewarming()) {
return true;
}
}
return false;
}
private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange( private static PositionUpdateForPlaylistChange resolvePositionForPlaylistChange(
Timeline timeline, Timeline timeline,
PlaybackInfo playbackInfo, PlaybackInfo playbackInfo,
@ -3416,16 +3521,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
: newTimeline.getPeriod(newPeriodIndex, period).windowIndex; : newTimeline.getPeriod(newPeriodIndex, period).windowIndex;
} }
private static Format[] getFormats(ExoTrackSelection newSelection) {
// Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0;
Format[] formats = new Format[length];
for (int i = 0; i < length; i++) {
formats[i] = newSelection.getFormat(i);
}
return formats;
}
private static final class SeekPosition { private static final class SeekPosition {
public final Timeline timeline; public final Timeline timeline;

View File

@ -18,9 +18,11 @@ package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Handler; import android.os.Handler;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -33,6 +35,10 @@ import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -82,6 +88,7 @@ import java.util.List;
private PreloadConfiguration preloadConfiguration; private PreloadConfiguration preloadConfiguration;
@Nullable private MediaPeriodHolder playing; @Nullable private MediaPeriodHolder playing;
@Nullable private MediaPeriodHolder reading; @Nullable private MediaPeriodHolder reading;
@Nullable private MediaPeriodHolder prewarming;
@Nullable private MediaPeriodHolder loading; @Nullable private MediaPeriodHolder loading;
@Nullable private MediaPeriodHolder preloading; @Nullable private MediaPeriodHolder preloading;
private int length; private int length;
@ -218,6 +225,7 @@ import java.util.List;
} else { } else {
playing = newPeriodHolder; playing = newPeriodHolder;
reading = newPeriodHolder; reading = newPeriodHolder;
prewarming = newPeriodHolder;
} }
oldFrontPeriodUid = null; oldFrontPeriodUid = null;
loading = newPeriodHolder; loading = newPeriodHolder;
@ -362,17 +370,37 @@ import java.util.List;
return reading; return reading;
} }
/** Returns the prewarming period holder, or null if the queue is empty. */
@Nullable
public MediaPeriodHolder getPrewarmingPeriod() {
return prewarming;
}
/** /**
* Continues reading from the next period holder in the queue. * Continues reading from the next period holder in the queue.
* *
* @return The updated reading period holder. * @return The updated reading period holder.
*/ */
public MediaPeriodHolder advanceReadingPeriod() { public MediaPeriodHolder advanceReadingPeriod() {
if (prewarming == reading) {
prewarming = checkStateNotNull(reading).getNext();
}
reading = checkStateNotNull(reading).getNext(); reading = checkStateNotNull(reading).getNext();
notifyQueueUpdate(); notifyQueueUpdate();
return checkStateNotNull(reading); return checkStateNotNull(reading);
} }
/**
* Continues pre-warming from the next period holder in the queue.
*
* @return The updated pre-warming period holder.
*/
public MediaPeriodHolder advancePrewarmingPeriod() {
prewarming = checkStateNotNull(prewarming).getNext();
notifyQueueUpdate();
return checkStateNotNull(prewarming);
}
/** /**
* Dequeues the playing period holder from the front of the queue and advances the playing period * Dequeues the playing period holder from the front of the queue and advances the playing period
* holder to be the next item in the queue. * holder to be the next item in the queue.
@ -387,6 +415,9 @@ import java.util.List;
if (playing == reading) { if (playing == reading) {
reading = playing.getNext(); reading = playing.getNext();
} }
if (playing == prewarming) {
prewarming = playing.getNext();
}
playing.release(); playing.release();
length--; length--;
if (length == 0) { if (length == 0) {
@ -405,27 +436,33 @@ import java.util.List;
* the same as the playing period holder at the front of the queue. * the same as the playing period holder at the front of the queue.
* *
* @param mediaPeriodHolder The media period holder that shall be the new end of the queue. * @param mediaPeriodHolder The media period holder that shall be the new end of the queue.
* @return Whether the reading period has been removed. * @return {@link RemoveAfterResult} with flags denoting if the reading or pre-warming periods
* were removed.
*/ */
public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { public int removeAfter(MediaPeriodHolder mediaPeriodHolder) {
checkStateNotNull(mediaPeriodHolder); checkStateNotNull(mediaPeriodHolder);
if (mediaPeriodHolder.equals(loading)) { if (mediaPeriodHolder.equals(loading)) {
return false; return 0;
} }
boolean removedReading = false; int removedResult = 0;
loading = mediaPeriodHolder; loading = mediaPeriodHolder;
while (mediaPeriodHolder.getNext() != null) { while (mediaPeriodHolder.getNext() != null) {
mediaPeriodHolder = checkNotNull(mediaPeriodHolder.getNext()); mediaPeriodHolder = checkNotNull(mediaPeriodHolder.getNext());
if (mediaPeriodHolder == reading) { if (mediaPeriodHolder == reading) {
reading = playing; reading = playing;
removedReading = true; prewarming = playing;
removedResult |= REMOVE_AFTER_REMOVED_READING_PERIOD;
}
if (mediaPeriodHolder == prewarming) {
prewarming = reading;
removedResult |= REMOVE_AFTER_REMOVED_PREWARMING_PERIOD;
} }
mediaPeriodHolder.release(); mediaPeriodHolder.release();
length--; length--;
} }
checkNotNull(loading).setNext(null); checkNotNull(loading).setNext(null);
notifyQueueUpdate(); notifyQueueUpdate();
return removedReading; return removedResult;
} }
/** /**
@ -472,6 +509,7 @@ import java.util.List;
playing = null; playing = null;
loading = null; loading = null;
reading = null; reading = null;
prewarming = null;
length = 0; length = 0;
notifyQueueUpdate(); notifyQueueUpdate();
} }
@ -511,11 +549,13 @@ import java.util.List;
getFollowingMediaPeriodInfo(timeline, previousPeriodHolder, rendererPositionUs); getFollowingMediaPeriodInfo(timeline, previousPeriodHolder, rendererPositionUs);
if (newPeriodInfo == null) { if (newPeriodInfo == null) {
// We've loaded a next media period that is not in the new timeline. // We've loaded a next media period that is not in the new timeline.
return !removeAfter(previousPeriodHolder); int removeAfterResult = removeAfter(previousPeriodHolder);
return (removeAfterResult & REMOVE_AFTER_REMOVED_READING_PERIOD) == 0;
} }
if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) { if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) {
// The new media period has a different id or start position. // The new media period has a different id or start position.
return !removeAfter(previousPeriodHolder); int removeAfterResult = removeAfter(previousPeriodHolder);
return (removeAfterResult & REMOVE_AFTER_REMOVED_READING_PERIOD) == 0;
} }
} }
@ -538,7 +578,9 @@ import java.util.List;
&& !periodHolder.info.isFollowedByTransitionToSameStream && !periodHolder.info.isFollowedByTransitionToSameStream
&& (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE
|| maxRendererReadPositionUs >= newDurationInRendererTime); || maxRendererReadPositionUs >= newDurationInRendererTime);
boolean readingPeriodRemoved = removeAfter(periodHolder); int removeAfterResult = removeAfter(periodHolder);
boolean readingPeriodRemoved =
(removeAfterResult & REMOVE_AFTER_REMOVED_READING_PERIOD) != 0;
return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration; return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration;
} }
@ -834,7 +876,8 @@ import java.util.List;
} }
// Release any period holders that don't match the new period order. // Release any period holders that don't match the new period order.
boolean readingPeriodRemoved = removeAfter(lastValidPeriodHolder); int removeAfterResult = removeAfter(lastValidPeriodHolder);
boolean readingPeriodRemoved = (removeAfterResult & REMOVE_AFTER_REMOVED_READING_PERIOD) != 0;
// Update the period info for the last holder, as it may now be the last period in the timeline. // Update the period info for the last holder, as it may now be the last period in the timeline.
lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info); lastValidPeriodHolder.info = getUpdatedMediaPeriodInfo(timeline, lastValidPeriodHolder.info);
@ -1211,4 +1254,22 @@ import java.util.List;
} }
return startPositionUs + period.getContentResumeOffsetUs(adGroupIndex); return startPositionUs + period.getContentResumeOffsetUs(adGroupIndex);
} }
/**
* Result for {@link #removeAfter} that signifies whether the reading or pre-warming periods were
* removed during the process.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
flag = true,
value = {REMOVE_AFTER_REMOVED_READING_PERIOD, REMOVE_AFTER_REMOVED_PREWARMING_PERIOD})
/* package */ @interface RemoveAfterResult {}
/** The call to {@link #removeAfter} removed the reading period. */
/* package */ static final int REMOVE_AFTER_REMOVED_READING_PERIOD = 1;
/** The call to {@link #removeAfter} removed the pre-warming period. */
/* package */ static final int REMOVE_AFTER_REMOVED_PREWARMING_PERIOD = 1 << 1;
} }

View File

@ -16,37 +16,84 @@
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.C.TRACK_TYPE_VIDEO; import static androidx.media3.common.C.TRACK_TYPE_VIDEO;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.exoplayer.Renderer.STATE_DISABLED; import static androidx.media3.exoplayer.Renderer.STATE_DISABLED;
import static androidx.media3.exoplayer.Renderer.STATE_ENABLED; import static androidx.media3.exoplayer.Renderer.STATE_ENABLED;
import static androidx.media3.exoplayer.Renderer.STATE_STARTED; import static androidx.media3.exoplayer.Renderer.STATE_STARTED;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.exoplayer.metadata.MetadataRenderer; import androidx.media3.exoplayer.metadata.MetadataRenderer;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.SampleStream;
import androidx.media3.exoplayer.text.TextRenderer; import androidx.media3.exoplayer.text.TextRenderer;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
/** Holds a {@link Renderer renderer}. */ /** Holds a {@link Renderer renderer}. */
/* package */ class RendererHolder { /* package */ class RendererHolder {
private final Renderer renderer; private final Renderer primaryRenderer;
// Index of renderer in renderer list held by the {@link Player}. // Index of renderer in renderer list held by the {@link Player}.
private final int index; private final int index;
private boolean requiresReset; @Nullable private final Renderer secondaryRenderer;
private @RendererPrewarmingState int prewarmingState;
private boolean primaryRequiresReset;
private boolean secondaryRequiresReset;
public RendererHolder(Renderer renderer, int index) { public RendererHolder(Renderer renderer, @Nullable Renderer secondaryRenderer, int index) {
this.renderer = renderer; this.primaryRenderer = renderer;
this.index = index; this.index = index;
requiresReset = false; this.secondaryRenderer = secondaryRenderer;
prewarmingState = RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY;
primaryRequiresReset = false;
secondaryRequiresReset = false;
}
public boolean hasSecondary() {
return secondaryRenderer != null;
}
public void startPrewarming() {
checkState(!isPrewarming());
prewarmingState =
isRendererEnabled(primaryRenderer)
? RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY
: secondaryRenderer != null && isRendererEnabled(secondaryRenderer)
? RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY
: RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY;
}
public boolean isPrewarming() {
return isPrimaryRendererPrewarming() || isSecondaryRendererPrewarming();
}
private boolean isPrimaryRendererPrewarming() {
return prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY;
}
private boolean isSecondaryRendererPrewarming() {
return prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY;
} }
public int getEnabledRendererCount() { public int getEnabledRendererCount() {
return isRendererEnabled(renderer) ? 1 : 0; int result = 0;
result += isRendererEnabled(primaryRenderer) ? 1 : 0;
result += secondaryRenderer != null && isRendererEnabled(secondaryRenderer) ? 1 : 0;
return result;
} }
/** /**
@ -55,7 +102,7 @@ import java.io.IOException;
* @see Renderer#getTrackType() * @see Renderer#getTrackType()
*/ */
public @C.TrackType int getTrackType() { public @C.TrackType int getTrackType() {
return renderer.getTrackType(); return primaryRenderer.getTrackType();
} }
/** /**
@ -70,8 +117,7 @@ import java.io.IOException;
* {@link MediaPeriodHolder media period}. * {@link MediaPeriodHolder media period}.
*/ */
public long getReadingPositionUs(@Nullable MediaPeriodHolder period) { public long getReadingPositionUs(@Nullable MediaPeriodHolder period) {
Assertions.checkState(isReadingFromPeriod(period)); return Objects.requireNonNull(getRendererReadingFromPeriod(period)).getReadingPositionUs();
return renderer.getReadingPositionUs();
} }
/** /**
@ -79,7 +125,8 @@ import java.io.IOException;
* *
* @see Renderer#hasReadStreamToEnd() * @see Renderer#hasReadStreamToEnd()
*/ */
public boolean hasReadStreamToEnd() { public boolean hasReadPeriodToEnd(MediaPeriodHolder mediaPeriodHolder) {
Renderer renderer = checkNotNull(getRendererReadingFromPeriod(mediaPeriodHolder));
return renderer.hasReadStreamToEnd(); return renderer.hasReadStreamToEnd();
} }
@ -88,47 +135,84 @@ import java.io.IOException;
* before it is next disabled or reset. * before it is next disabled or reset.
* *
* @see Renderer#setCurrentStreamFinal() * @see Renderer#setCurrentStreamFinal()
* @param mediaPeriodHolder The {@link MediaPeriodHolder media period} containing the current
* stream.
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to * @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream. * render until the end of the current stream.
*/ */
public void setCurrentStreamFinal(long streamEndPositionUs) { public void setCurrentStreamFinal(MediaPeriodHolder mediaPeriodHolder, long streamEndPositionUs) {
if (renderer.getStream() != null) { Renderer renderer = checkNotNull(getRendererReadingFromPeriod(mediaPeriodHolder));
setCurrentStreamFinal(renderer, streamEndPositionUs); setCurrentStreamFinalInternal(renderer, streamEndPositionUs);
}
/**
* Maybe signal to the renderer that the old {@link SampleStream} will be the final one supplied
* before it is next disabled or reset.
*
* @param oldTrackSelectorResult {@link TrackSelectorResult} containing the previous {@link
* SampleStream}.
* @param newTrackSelectorResult {@link TrackSelectorResult} containing the next {@link
* SampleStream}.
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
public void maybeSetOldStreamToFinal(
TrackSelectorResult oldTrackSelectorResult,
TrackSelectorResult newTrackSelectorResult,
long streamEndPositionUs) {
boolean oldRendererEnabled = oldTrackSelectorResult.isRendererEnabled(index);
boolean newRendererEnabled = newTrackSelectorResult.isRendererEnabled(index);
boolean isPrimaryOldRenderer =
secondaryRenderer == null
|| prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY
|| (prewarmingState == RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY
&& isRendererEnabled(primaryRenderer));
Renderer oldRenderer = isPrimaryOldRenderer ? primaryRenderer : checkNotNull(secondaryRenderer);
if (oldRendererEnabled && !oldRenderer.isCurrentStreamFinal()) {
boolean isNoSampleRenderer = getTrackType() == C.TRACK_TYPE_NONE;
RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[index];
RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[index];
if (!newRendererEnabled
|| !Objects.equals(newConfig, oldConfig)
|| isNoSampleRenderer
|| isPrewarming()) {
// The renderer will be disabled when transitioning to playing the next period, because
// there's no new selection, or because a configuration change is required, or because
// it's a no-sample renderer for which rendererOffsetUs should be updated only when
// starting to play the next period, or there is a backup renderer that has already been
// enabled for the following media item. Mark the SampleStream as final to play out any
// remaining data.
setCurrentStreamFinalInternal(oldRenderer, streamEndPositionUs);
}
} }
} }
private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) { /**
* Calls {@link Renderer#setCurrentStreamFinal} on enabled {@link Renderer renderers} that are not
* pre-warming.
*
* @see Renderer#setCurrentStreamFinal
*/
public void setAllNonPrewarmingRendererStreamsFinal(long streamEndPositionUs) {
if (isRendererEnabled(primaryRenderer)
&& prewarmingState != RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY
&& prewarmingState != RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY) {
setCurrentStreamFinalInternal(primaryRenderer, streamEndPositionUs);
}
if (secondaryRenderer != null
&& isRendererEnabled(secondaryRenderer)
&& prewarmingState != RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY) {
setCurrentStreamFinalInternal(secondaryRenderer, streamEndPositionUs);
}
}
private void setCurrentStreamFinalInternal(Renderer renderer, long streamEndPositionUs) {
renderer.setCurrentStreamFinal(); renderer.setCurrentStreamFinal();
if (renderer instanceof TextRenderer) { if (renderer instanceof TextRenderer) {
((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs); ((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} * Returns minimum amount of playback clock time that must pass in order for the {@link #render}
* call to make progress. * call to make progress.
@ -145,9 +229,19 @@ import java.io.IOException;
*/ */
public long getMinDurationToProgressUs( public long getMinDurationToProgressUs(
long rendererPositionUs, long rendererPositionElapsedRealtimeUs) { long rendererPositionUs, long rendererPositionElapsedRealtimeUs) {
return isRendererEnabled(renderer) long minDurationToProgress =
? renderer.getDurationToProgressUs(rendererPositionUs, rendererPositionElapsedRealtimeUs) isRendererEnabled(primaryRenderer)
: Long.MAX_VALUE; ? primaryRenderer.getDurationToProgressUs(
rendererPositionUs, rendererPositionElapsedRealtimeUs)
: Long.MAX_VALUE;
if (secondaryRenderer != null && isRendererEnabled(secondaryRenderer)) {
minDurationToProgress =
min(
minDurationToProgress,
secondaryRenderer.getDurationToProgressUs(
rendererPositionUs, rendererPositionElapsedRealtimeUs));
}
return minDurationToProgress;
} }
/** /**
@ -156,8 +250,10 @@ import java.io.IOException;
* @see Renderer#enableMayRenderStartOfStream * @see Renderer#enableMayRenderStartOfStream
*/ */
public void enableMayRenderStartOfStream() { public void enableMayRenderStartOfStream() {
if (isRendererEnabled(renderer)) { if (isRendererEnabled(primaryRenderer)) {
renderer.enableMayRenderStartOfStream(); primaryRenderer.enableMayRenderStartOfStream();
} else if (secondaryRenderer != null && isRendererEnabled(secondaryRenderer)) {
secondaryRenderer.enableMayRenderStartOfStream();
} }
} }
@ -168,7 +264,10 @@ import java.io.IOException;
*/ */
public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed) public void setPlaybackSpeed(float currentPlaybackSpeed, float targetPlaybackSpeed)
throws ExoPlaybackException { throws ExoPlaybackException {
renderer.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed); primaryRenderer.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed);
if (secondaryRenderer != null) {
secondaryRenderer.setPlaybackSpeed(currentPlaybackSpeed, targetPlaybackSpeed);
}
} }
/** /**
@ -177,7 +276,10 @@ import java.io.IOException;
* @see Renderer#setTimeline * @see Renderer#setTimeline
*/ */
public void setTimeline(Timeline timeline) { public void setTimeline(Timeline timeline) {
renderer.setTimeline(timeline); primaryRenderer.setTimeline(timeline);
if (secondaryRenderer != null) {
secondaryRenderer.setTimeline(timeline);
}
} }
/** /**
@ -187,7 +289,14 @@ import java.io.IOException;
* @return if all renderers have {@link Renderer#isEnded() ended}. * @return if all renderers have {@link Renderer#isEnded() ended}.
*/ */
public boolean isEnded() { public boolean isEnded() {
return renderer.isEnded(); boolean renderersEnded = true;
if (isRendererEnabled(primaryRenderer)) {
renderersEnded &= primaryRenderer.isEnded();
}
if (secondaryRenderer != null && isRendererEnabled(secondaryRenderer)) {
renderersEnded &= secondaryRenderer.isEnded();
}
return renderersEnded;
} }
/** /**
@ -200,28 +309,6 @@ import java.io.IOException;
return getRendererReadingFromPeriod(period) != null; 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 * Returns whether the {@link Renderer renderers} are still reading a {@link MediaPeriodHolder
* media period}. * media period}.
@ -230,17 +317,27 @@ import java.io.IOException;
* @return true if {@link Renderer renderers} are reading the current reading period. * @return true if {@link Renderer renderers} are reading the current reading period.
*/ */
public boolean hasFinishedReadingFromPeriod(MediaPeriodHolder periodHolder) { public boolean hasFinishedReadingFromPeriod(MediaPeriodHolder periodHolder) {
return hasFinishedReadingFromPeriodInternal(periodHolder); return hasFinishedReadingFromPeriodInternal(periodHolder, primaryRenderer)
&& hasFinishedReadingFromPeriodInternal(periodHolder, secondaryRenderer);
} }
private boolean hasFinishedReadingFromPeriodInternal(MediaPeriodHolder readingPeriodHolder) { private boolean hasFinishedReadingFromPeriodInternal(
MediaPeriodHolder readingPeriodHolder, @Nullable Renderer renderer) {
if (renderer == null) {
return true;
}
SampleStream sampleStream = readingPeriodHolder.sampleStreams[index]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[index];
if (renderer.getStream() != sampleStream if (renderer.getStream() != null
|| (sampleStream != null && (renderer.getStream() != sampleStream
&& !renderer.hasReadStreamToEnd() || (sampleStream != null
&& !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder))) { && !renderer.hasReadStreamToEnd()
&& !hasReachedServerSideInsertedAdsTransition(renderer, readingPeriodHolder)))) {
// The current reading period is still being read by at least one renderer. // The current reading period is still being read by at least one renderer.
return false; MediaPeriodHolder followingPeriod = readingPeriodHolder.getNext();
// If renderer is reading ahead as it was enabled early, then it is not 'reading' the
// current reading period.
return followingPeriod != null
&& followingPeriod.sampleStreams[index] == renderer.getStream();
} }
return true; return true;
} }
@ -272,8 +369,11 @@ import java.io.IOException;
*/ */
public void render(long rendererPositionUs, long rendererPositionElapsedRealtimeUs) public void render(long rendererPositionUs, long rendererPositionElapsedRealtimeUs)
throws ExoPlaybackException { throws ExoPlaybackException {
if (isRendererEnabled(renderer)) { if (isRendererEnabled(primaryRenderer)) {
renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); primaryRenderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
}
if (secondaryRenderer != null && isRendererEnabled(secondaryRenderer)) {
secondaryRenderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs);
} }
} }
@ -289,23 +389,22 @@ import java.io.IOException;
* @param playingPeriodHolder The currently playing media {@link MediaPeriodHolder period}. * @param playingPeriodHolder The currently playing media {@link MediaPeriodHolder period}.
* @return whether renderer allows playback. * @return whether renderer allows playback.
*/ */
public boolean allowsPlayback(MediaPeriodHolder playingPeriodHolder) throws IOException { public boolean allowsPlayback(MediaPeriodHolder playingPeriodHolder) {
return allowsPlayback(renderer, playingPeriodHolder); Renderer renderer = getRendererReadingFromPeriod(playingPeriodHolder);
} return renderer == null
|| renderer.hasReadStreamToEnd()
private boolean allowsPlayback(Renderer renderer, MediaPeriodHolder playingPeriodHolder) { || renderer.isReady()
boolean isReadingAhead = playingPeriodHolder.sampleStreams[index] != renderer.getStream(); || renderer.isEnded();
boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd();
return isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded();
} }
/** /**
* Invokes {Renderer#maybeThrowStreamError}. * Invokes {@link Renderer#maybeThrowStreamError()} for {@link Renderer} enabled on {@link
* MediaPeriodHolder media period}.
* *
* @see Renderer#maybeThrowStreamError() * @see Renderer#maybeThrowStreamError()
*/ */
public void maybeThrowStreamError() throws IOException { public void maybeThrowStreamError(MediaPeriodHolder mediaPeriodHolder) throws IOException {
renderer.maybeThrowStreamError(); checkNotNull(getRendererReadingFromPeriod(mediaPeriodHolder)).maybeThrowStreamError();
} }
/** /**
@ -314,15 +413,23 @@ import java.io.IOException;
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
public void start() throws ExoPlaybackException { public void start() throws ExoPlaybackException {
if (renderer.getState() == STATE_ENABLED) { if (primaryRenderer.getState() == STATE_ENABLED
renderer.start(); && (prewarmingState != RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY)) {
primaryRenderer.start();
} else if (secondaryRenderer != null
&& secondaryRenderer.getState() == STATE_ENABLED
&& prewarmingState != RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY) {
secondaryRenderer.start();
} }
} }
/** Calls {@link Renderer#stop()} on all enabled {@link Renderer renderers}. */ /** Calls {@link Renderer#stop()} on all enabled {@link Renderer renderers}. */
public void stop() { public void stop() {
if (isRendererEnabled(renderer)) { if (isRendererEnabled(primaryRenderer)) {
ensureStopped(renderer); ensureStopped(primaryRenderer);
}
if (secondaryRenderer != null && isRendererEnabled(secondaryRenderer)) {
ensureStopped(secondaryRenderer);
} }
} }
@ -337,7 +444,7 @@ import java.io.IOException;
* *
* @see Renderer#enable * @see Renderer#enable
* @param configuration The renderer configuration. * @param configuration The renderer configuration.
* @param formats The enabled formats. * @param trackSelection The track selection for the {@link Renderer}.
* @param stream The {@link SampleStream} from which the renderer should consume. * @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position. * @param positionUs The player's current position.
* @param joining Whether this renderer is being enabled to join an ongoing playback. * @param joining Whether this renderer is being enabled to join an ongoing playback.
@ -354,7 +461,7 @@ import java.io.IOException;
*/ */
public void enable( public void enable(
RendererConfiguration configuration, RendererConfiguration configuration,
Format[] formats, ExoTrackSelection trackSelection,
SampleStream stream, SampleStream stream,
long positionUs, long positionUs,
boolean joining, boolean joining,
@ -364,27 +471,53 @@ import java.io.IOException;
MediaSource.MediaPeriodId mediaPeriodId, MediaSource.MediaPeriodId mediaPeriodId,
DefaultMediaClock mediaClock) DefaultMediaClock mediaClock)
throws ExoPlaybackException { throws ExoPlaybackException {
requiresReset = true; Format[] formats = getFormats(trackSelection);
renderer.enable( boolean enablePrimary =
configuration, prewarmingState == RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY
formats, || prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
stream, || prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY;
positionUs, if (enablePrimary) {
joining, primaryRequiresReset = true;
mayRenderStartOfStream, primaryRenderer.enable(
startPositionUs, configuration,
offsetUs, formats,
mediaPeriodId); stream,
mediaClock.onRendererEnabled(renderer); positionUs,
joining,
mayRenderStartOfStream,
startPositionUs,
offsetUs,
mediaPeriodId);
mediaClock.onRendererEnabled(primaryRenderer);
} else {
secondaryRequiresReset = true;
checkNotNull(secondaryRenderer)
.enable(
configuration,
formats,
stream,
positionUs,
joining,
mayRenderStartOfStream,
startPositionUs,
offsetUs,
mediaPeriodId);
mediaClock.onRendererEnabled(secondaryRenderer);
}
} }
/** /**
* Invokes {@link Renderer#handleMessage} on the {@link Renderer}. * Invokes {@link Renderer#handleMessage} on the {@link Renderer} enabled on the {@link
* MediaPeriodHolder media period}.
* *
* @see Renderer#handleMessage(int, Object) * @see Renderer#handleMessage(int, Object)
*/ */
public void handleMessage(@Renderer.MessageType int messageType, @Nullable Object message) public void handleMessage(
@Renderer.MessageType int messageType,
@Nullable Object message,
MediaPeriodHolder mediaPeriod)
throws ExoPlaybackException { throws ExoPlaybackException {
Renderer renderer = checkNotNull(getRendererReadingFromPeriod(mediaPeriod));
renderer.handleMessage(messageType, message); renderer.handleMessage(messageType, message);
} }
@ -395,7 +528,22 @@ import java.io.IOException;
* Renderer}. * Renderer}.
*/ */
public void disable(DefaultMediaClock mediaClock) { public void disable(DefaultMediaClock mediaClock) {
disableRenderer(renderer, mediaClock); disableRenderer(primaryRenderer, mediaClock);
if (secondaryRenderer != null) {
disableRenderer(secondaryRenderer, mediaClock);
// Release resources for other renderer
maybeResetRenderer(/* resetPrimary= */ false);
}
prewarmingState = RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY;
}
public void maybeHandlePrewarmingTransition() {
if (prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY) {
prewarmingState = RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY;
} else if (prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY) {
prewarmingState = RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_SECONDARY;
}
} }
/** /**
@ -410,6 +558,7 @@ import java.io.IOException;
* renderer}. * renderer}.
*/ */
private void disableRenderer(Renderer renderer, DefaultMediaClock mediaClock) { private void disableRenderer(Renderer renderer, DefaultMediaClock mediaClock) {
checkState(primaryRenderer == renderer || secondaryRenderer == renderer);
if (!isRendererEnabled(renderer)) { if (!isRendererEnabled(renderer)) {
return; return;
} }
@ -419,37 +568,241 @@ import java.io.IOException;
} }
/** /**
* Calls {@link Renderer#resetPosition} on the {@link Renderer} if its enabled. * Invokes {@link Renderer#resetPosition} on the {@link Renderer} that is enabled on the provided
* {@link MediaPeriodHolder media period}.
* *
* @see Renderer#resetPosition * @see Renderer#resetPosition
*/ */
public void resetPosition(long positionUs) throws ExoPlaybackException { public void resetPosition(MediaPeriodHolder playingPeriod, long positionUs)
if (isRendererEnabled(renderer)) { throws ExoPlaybackException {
Renderer renderer = getRendererReadingFromPeriod(playingPeriod);
if (renderer != null) {
renderer.resetPosition(positionUs); renderer.resetPosition(positionUs);
} }
} }
/** Calls {@link Renderer#reset()} on all renderers that must be reset. */ /**
* Calls {@link Renderer#reset()} on all disabled {@link Renderer renderers} that must be reset.
*/
public void reset() { public void reset() {
if (requiresReset) { if (!isRendererEnabled(primaryRenderer)) {
renderer.reset(); maybeResetRenderer(/* resetPrimary= */ true);
requiresReset = false;
} }
if (secondaryRenderer != null && !isRendererEnabled(secondaryRenderer)) {
maybeResetRenderer(/* resetPrimary= */ false);
}
}
private void maybeResetRenderer(boolean resetPrimary) {
if (resetPrimary) {
if (primaryRequiresReset) {
primaryRenderer.reset();
primaryRequiresReset = false;
}
} else if (secondaryRequiresReset) {
checkNotNull(secondaryRenderer).reset();
secondaryRequiresReset = false;
}
}
public int replaceStreamsOrDisableRendererForTransition(
MediaPeriodHolder readingPeriodHolder,
TrackSelectorResult newTrackSelectorResult,
DefaultMediaClock mediaClock)
throws ExoPlaybackException {
int primaryRendererResult =
replaceStreamsOrDisableRendererForTransitionInternal(
primaryRenderer, readingPeriodHolder, newTrackSelectorResult, mediaClock);
int secondaryRendererResult =
replaceStreamsOrDisableRendererForTransitionInternal(
secondaryRenderer, readingPeriodHolder, newTrackSelectorResult, mediaClock);
return primaryRendererResult == REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED
? secondaryRendererResult
: primaryRendererResult;
}
private int replaceStreamsOrDisableRendererForTransitionInternal(
@Nullable Renderer renderer,
MediaPeriodHolder readingPeriodHolder,
TrackSelectorResult newTrackSelectorResult,
DefaultMediaClock mediaClock)
throws ExoPlaybackException {
if (renderer == null
|| !isRendererEnabled(renderer)
|| (renderer == primaryRenderer && isPrimaryRendererPrewarming())
|| (renderer == secondaryRenderer && isSecondaryRendererPrewarming())) {
return REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED;
}
boolean rendererIsReadingOldStream =
renderer.getStream() != readingPeriodHolder.sampleStreams[index];
boolean rendererShouldBeEnabled = newTrackSelectorResult.isRendererEnabled(index);
if (rendererShouldBeEnabled && !rendererIsReadingOldStream) {
// All done.
return REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED;
}
if (!renderer.isCurrentStreamFinal()) {
// The renderer stream is not final, so we can replace the sample streams immediately.
Format[] formats = getFormats(newTrackSelectorResult.selections[index]);
renderer.replaceStream(
formats,
checkNotNull(readingPeriodHolder.sampleStreams[index]),
readingPeriodHolder.getStartPositionRendererTime(),
readingPeriodHolder.getRendererOffset(),
readingPeriodHolder.info.id);
// Prevent sleeping across offload track transition else position won't get updated.
return REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED
| REPLACE_STREAMS_DISABLE_RENDERERS_DISABLE_OFFLOAD_SCHEDULING;
} else if (renderer.isEnded()) {
// The renderer has finished playback, so we can disable it now.
disableRenderer(renderer, mediaClock);
if (!rendererShouldBeEnabled || isPrewarming()) {
maybeResetRenderer(/* resetPrimary= */ renderer == primaryRenderer);
}
return REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED;
} else {
// Need to wait until rendering of current item is finished.
return 0;
}
}
private static Format[] getFormats(@Nullable ExoTrackSelection newSelection) {
// Build an array of formats contained by the selection.
int length = newSelection != null ? newSelection.length() : 0;
Format[] formats = new Format[length];
for (int i = 0; i < length; i++) {
formats[i] = checkNotNull(newSelection).getFormat(i);
}
return formats;
} }
/** Calls {@link Renderer#release()} on all {@link Renderer renderers}. */ /** Calls {@link Renderer#release()} on all {@link Renderer renderers}. */
public void release() { public void release() {
renderer.release(); primaryRenderer.release();
requiresReset = false; primaryRequiresReset = false;
if (secondaryRenderer != null) {
secondaryRenderer.release();
secondaryRequiresReset = false;
}
} }
public void setVideoOutput(@Nullable Object videoOutput) throws ExoPlaybackException { public void setVideoOutput(@Nullable Object videoOutput) throws ExoPlaybackException {
if (renderer.getTrackType() == TRACK_TYPE_VIDEO) { if (getTrackType() != TRACK_TYPE_VIDEO) {
renderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, videoOutput); return;
} }
if (prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_SECONDARY) {
checkNotNull(secondaryRenderer).handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, videoOutput);
} else {
primaryRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, videoOutput);
}
}
public boolean isRendererEnabled() {
boolean checkPrimary =
prewarmingState == RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY
|| prewarmingState == RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY;
return checkPrimary
? isRendererEnabled(primaryRenderer)
: isRendererEnabled(checkNotNull(secondaryRenderer));
} }
private static boolean isRendererEnabled(Renderer renderer) { private static boolean isRendererEnabled(Renderer renderer) {
return renderer.getState() != STATE_DISABLED; return renderer.getState() != STATE_DISABLED;
} }
/**
* 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 (primaryRenderer.getStream() == period.sampleStreams[index]) {
return primaryRenderer;
} else if (secondaryRenderer != null
&& secondaryRenderer.getStream() == period.sampleStreams[index]) {
return secondaryRenderer;
}
return null;
}
/** Possible pre-warming states for primary and secondary renderers. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY,
RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_SECONDARY,
RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY,
RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY,
RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY
})
@interface RendererPrewarmingState {}
/**
* ExoPlayer is not currently transitioning between two enabled renderers for subsequent media
* items and is using the primary renderer.
*/
/* package */ static final int RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_PRIMARY = 0;
/**
* ExoPlayer is not currently transitioning between two enabled renderers for subsequent media
* items and is using the secondary renderer.
*/
/* package */ static final int RENDERER_PREWARMING_STATE_NOT_PREWARMING_USING_SECONDARY = 1;
/**
* ExoPlayer is currently pre-warming the primary renderer that is not being used for the current
* media item for a subsequent media item.
*/
/* package */ static final int RENDERER_PREWARMING_STATE_PREWARMING_PRIMARY = 2;
/**
* Both a primary and secondary renderer are enabled and ExoPlayer is transitioning to a media
* item using the secondary renderer.
*/
/* package */ static final int RENDERER_PREWARMING_STATE_TRANSITIONING_TO_SECONDARY = 3;
/**
* Both a primary and secondary renderer are enabled and ExoPlayer is transitioning to a media
* item using the primary renderer.
*/
/* package */ static final int RENDERER_PREWARMING_STATE_TRANSITIONING_TO_PRIMARY = 4;
/** Results for calls to {@link #replaceStreamsOrDisableRendererForTransition}. */
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
flag = true,
value = {
REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED,
REPLACE_STREAMS_DISABLE_RENDERERS_DISABLE_OFFLOAD_SCHEDULING
})
/* package */ @interface ReplaceStreamsOrDisableRendererResult {}
/**
* The call to {@link #replaceStreamsOrDisableRendererForTransition} has completed processing
* {@link Renderer#replaceStream} or {@link Renderer#disable()} on all renderers enabled on the
* current playing period.
*/
/* package */ static final int REPLACE_STREAMS_DISABLE_RENDERERS_COMPLETED = 1;
/**
* The call to {@link #replaceStreamsOrDisableRendererForTransition} invoked {@link
* Renderer#replaceStream} and so therefore offload should be disabled until after the media
* transition.
*/
/* package */ static final int REPLACE_STREAMS_DISABLE_RENDERERS_DISABLE_OFFLOAD_SCHEDULING =
1 << 1;
} }

View File

@ -16,6 +16,7 @@
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import android.os.Handler; import android.os.Handler;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.metadata.MetadataOutput; import androidx.media3.exoplayer.metadata.MetadataOutput;
@ -42,4 +43,29 @@ public interface RenderersFactory {
AudioRendererEventListener audioRendererEventListener, AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput, TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput); MetadataOutput metadataRendererOutput);
/**
* Provides a secondary {@link Renderer} instance for an {@link ExoPlayer} to use for pre-warming.
*
* <p>The created secondary {@code Renderer} should match its primary in its reported track type
* support and {@link RendererCapabilities}.
*
* @param renderer The primary {@code Renderer} for which to create the backup.
* @param eventHandler A handler to use when invoking event listeners and outputs.
* @param videoRendererEventListener An event listener for video renderers.
* @param audioRendererEventListener An event listener for audio renderers.
* @param textRendererOutput An output for text renderers.
* @param metadataRendererOutput An output for metadata renderers.
* @return The {@link Renderer instances}.
*/
@Nullable
default Renderer createSecondaryRenderer(
Renderer renderer,
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
return null;
}
} }

View File

@ -0,0 +1,331 @@
/*
* Copyright (C) 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.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.Handler;
import androidx.media3.common.Player;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import androidx.media3.test.utils.ExoPlayerTestRunner;
import androidx.media3.test.utils.FakeAudioRenderer;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.FakeMediaSource;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.FakeVideoRenderer;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link ExoPlayer} with the pre-warming render feature. */
@RunWith(AndroidJUnit4.class)
public class ExoPlayerWithPrewarmingRenderersTest {
private Context context;
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Before
public void setUp() {
context = ApplicationProvider.getApplicationContext();
}
@Test
public void play_withTwoItemsAndPrewarming_secondaryRendererisEnabledButNotStarted()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock))
.build();
Renderer videoRenderer = player.getRenderer(/* index= */ 0);
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
// Set a playlist that allows a new renderer to be enabled early.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
// Play a bit until the second renderer is being pre-warmed.
player.play();
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
// Play until second item is started.
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_STARTED);
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
player.release();
assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_DISABLED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED);
}
@Test
public void
play_withThreeItemsAndPrewarming_playerSuccessfullyPrewarmsAndSwapsBackToPrimaryRenderer()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock))
.build();
Renderer videoRenderer = player.getRenderer(/* index= */ 0);
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
// Set a playlist that allows a new renderer to be enabled early.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
// Play a bit until the secondary renderer is being pre-warmed.
player.play();
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
// Play until until the primary renderer is being pre-warmed.
run(player)
.untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
// Play until past transition back to primary renderer for third media item.
run(player)
.untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_STARTED);
@Renderer.State int videoState3 = videoRenderer.getState();
@Renderer.State int secondaryVideoState3 = secondaryVideoRenderer.getState();
player.release();
assertThat(videoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED);
assertThat(videoState3).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_STARTED);
assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_DISABLED);
}
@Test
public void prepare_withPeriodBetweenPlayingAndPrewarmingPeriods_playerSuccessfullyPrewarms()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock))
.build();
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
// Set a playlist that allows a new renderer to be enabled early.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
// Advance media periods until secondary renderer is being pre-warmed.
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int secondaryVideoState = secondaryVideoRenderer.getState();
player.release();
assertThat(secondaryVideoState).isEqualTo(Renderer.STATE_ENABLED);
}
@Test
public void
setPlayWhenReady_playFromPauseWithPrewarmingPrimaryRenderer_primaryRendererIsEnabledButNotStarted()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock))
.build();
Renderer videoRenderer = player.getRenderer(/* index= */ 0);
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
// Set a playlist that allows a new renderer to be enabled early.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
// Play a bit until the primary renderer has been enabled, but not yet started.
player.play();
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_STARTED);
run(player)
.untilBackgroundThreadCondition(() -> videoRenderer.getState() == Renderer.STATE_ENABLED);
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
player.pause();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState2 = videoRenderer.getState();
@Renderer.State int secondaryVideoState2 = secondaryVideoRenderer.getState();
player.play();
run(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_STARTED);
@Renderer.State int videoState3 = videoRenderer.getState();
@Renderer.State int secondaryVideoState3 = secondaryVideoRenderer.getState();
player.release();
assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_STARTED);
assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState2).isEqualTo(Renderer.STATE_ENABLED);
assertThat(videoState3).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_STARTED);
}
@Test
public void
setPlayWhenReady_playFromPauseWithPrewarmingNonTransitioningRenderer_rendererIsEnabledButNotStarted()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(
new FakeRenderersFactorySupportingSecondaryVideoRenderer(fakeClock))
.build();
Renderer videoRenderer = player.getRenderer(/* index= */ 0);
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
// Set a playlist that allows a new renderer to be enabled early.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT)));
player.prepare();
// Play a bit until the primary renderer has been enabled, but not yet started.
run(player).untilState(Player.STATE_READY);
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState1 = videoRenderer.getState();
@Renderer.State int secondaryVideoState1 = secondaryVideoRenderer.getState();
player.pause();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState2 = videoRenderer.getState();
player.play();
run(player).untilPendingCommandsAreFullyHandled();
@Renderer.State int videoState3 = videoRenderer.getState();
player.release();
assertThat(videoState1).isEqualTo(Renderer.STATE_ENABLED);
assertThat(secondaryVideoState1).isEqualTo(Renderer.STATE_DISABLED);
assertThat(videoState2).isEqualTo(Renderer.STATE_ENABLED);
assertThat(videoState3).isEqualTo(Renderer.STATE_ENABLED);
}
private static class FakeRenderersFactorySupportingSecondaryVideoRenderer
implements RenderersFactory {
protected final Clock clock;
public FakeRenderersFactorySupportingSecondaryVideoRenderer(Clock clock) {
this.clock = clock;
}
@Override
public Renderer[] createRenderers(
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
HandlerWrapper clockAwareHandler =
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
return new Renderer[] {
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
}
@Override
public Renderer createSecondaryRenderer(
Renderer renderer,
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
if (renderer instanceof FakeVideoRenderer) {
return new FakeVideoRenderer(
clock.createHandler(eventHandler.getLooper(), /* callback= */ null),
videoRendererEventListener);
}
return null;
}
}
}

View File

@ -0,0 +1,315 @@
/*
* Copyright (C) 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.e2etest;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.os.Handler;
import android.view.Surface;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.image.ImageDecoder;
import androidx.media3.exoplayer.image.ImageRenderer;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.exoplayer.video.MediaCodecVideoRenderer;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import androidx.media3.test.utils.CapturingRenderersFactory;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.robolectric.PlaybackOutput;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
/** End-to-end playback tests using secondary renderers. */
@RunWith(AndroidJUnit4.class)
public class PrewarmingRendererPlaybackTest {
private static final String TEST_AUDIO_MP4_URI = "asset:///media/mp4/sample_ac3.mp4";
private static final String TEST_MP4_URI = "asset:///media/mp4/sample.mp4";
private static final String TEST_IMAGE_URI = "asset:///media/jpeg/tokyo.jpg";
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Test
public void playback_withTwoMediaItemsAndSecondaryVideoRenderer_dumpsCorrectOutput()
throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersWithSecondaryVideoRendererFactory capturingRenderersFactory =
new CapturingRenderersWithSecondaryVideoRendererFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaItems(
ImmutableList.of(
new MediaItem.Builder().setUri(TEST_MP4_URI).build(),
new MediaItem.Builder().setUri(TEST_MP4_URI).build()));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
/* dumpFile= */ "playbackdumps/prewarmingRenderer/"
+ "twoItemsPlaylist-withSecondaryVideoRenderer"
+ ".dump");
}
@Test
public void playback_withThreeItemsAndSecondaryVideoRenderer_dumpsCorrectOutput()
throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersWithSecondaryVideoRendererFactory capturingRenderersFactory =
new CapturingRenderersWithSecondaryVideoRendererFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaItems(
ImmutableList.of(
new MediaItem.Builder().setUri(TEST_MP4_URI).build(),
new MediaItem.Builder().setUri(TEST_MP4_URI).build(),
new MediaItem.Builder().setUri(TEST_MP4_URI).build()));
// Disable audio renderer for simpler dump file.
player.setTrackSelectionParameters(
player
.getTrackSelectionParameters()
.buildUpon()
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, true)
.build());
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
"playbackdumps/prewarmingRenderer/"
+ "threeItemPlaylist-withSecondaryVideoRenderer"
+ ".dump");
}
@Test
public void playback_withMultipleMediaItemsWithClippingConfigurations_dumpsCorrectOutput()
throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersWithSecondaryVideoRendererFactory capturingRenderersFactory =
new CapturingRenderersWithSecondaryVideoRendererFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaItem(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(0)
.setEndPositionMs(250)
.build())
.build());
player.addMediaItem(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(250)
.setEndPositionMs(600)
.build())
.build());
player.addMediaItem(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(600)
.setEndPositionMs(C.TIME_END_OF_SOURCE)
.build())
.build());
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
"playbackdumps/prewarmingRenderer/"
+ "threeItemPlaylist-withClippingConfigurations"
+ ".dump");
}
@Test
public void playback_withPrewarmingNonTransitioningRenderer_dumpsCorrectOutput()
throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersWithSecondaryVideoRendererFactory capturingRenderersFactory =
new CapturingRenderersWithSecondaryVideoRendererFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaItem(new MediaItem.Builder().setUri(TEST_AUDIO_MP4_URI).build());
player.addMediaItem(
new MediaItem.Builder()
.setUri(TEST_MP4_URI)
.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(300)
.setEndPositionMs(600)
.build())
.build());
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
"playbackdumps/prewarmingRenderer/"
+ "twoItemPlaylist-withPrewarmingNonTransitioningRenderer"
+ ".dump");
}
@Test
public void playback_withImageVideoPlaylistAndSecondaryVideoRendererOnly_dumpsCorrectOutput()
throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersWithSecondaryVideoAndImageRenderersFactory capturingRenderersFactory =
new CapturingRenderersWithSecondaryVideoAndImageRenderersFactory(applicationContext);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, capturingRenderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
Surface surface = new Surface(new SurfaceTexture(/* texName= */ 1));
player.setVideoSurface(surface);
PlaybackOutput playbackOutput = PlaybackOutput.register(player, capturingRenderersFactory);
player.addMediaItems(
ImmutableList.of(
new MediaItem.Builder().setUri(TEST_IMAGE_URI).setImageDurationMs(1000).build(),
new MediaItem.Builder().setUri(TEST_MP4_URI).build(),
new MediaItem.Builder().setUri(TEST_IMAGE_URI).setImageDurationMs(1000).build()));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
surface.release();
DumpFileAsserts.assertOutput(
applicationContext,
playbackOutput,
/* dumpFile= */ "playbackdumps/prewarmingRenderer/"
+ "imageVideoPlaylist-withSecondaryRenderers"
+ ".dump");
}
/** This class extends {@link CapturingRenderersFactory} to provide a secondary video renderer. */
private static final class CapturingRenderersWithSecondaryVideoRendererFactory
extends CapturingRenderersFactory {
/**
* Creates an instance.
*
* @param context The {@link Context}.
*/
public CapturingRenderersWithSecondaryVideoRendererFactory(Context context) {
super(context);
}
@Override
public Renderer createSecondaryRenderer(
Renderer renderer,
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
if (renderer.getClass().getSuperclass() == MediaCodecVideoRenderer.class) {
return createMediaCodecVideoRenderer(eventHandler, videoRendererEventListener);
}
return null;
}
}
/** This class extends {@link CapturingRenderersFactory} to provide a secondary video renderer. */
private static final class CapturingRenderersWithSecondaryVideoAndImageRenderersFactory
extends CapturingRenderersFactory {
/**
* Creates an instance.
*
* @param context The {@link Context}.
*/
public CapturingRenderersWithSecondaryVideoAndImageRenderersFactory(Context context) {
super(context);
}
@Override
public Renderer createSecondaryRenderer(
Renderer renderer,
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
if (renderer.getClass().getSuperclass() == MediaCodecVideoRenderer.class) {
return createMediaCodecVideoRenderer(eventHandler, videoRendererEventListener);
} else if (renderer.getClass() == ImageRenderer.class) {
return new ImageRenderer(ImageDecoder.Factory.DEFAULT, /* imageOutput= */ null);
}
return null;
}
}
}

View File

@ -0,0 +1,692 @@
MediaCodecAdapter (exotest.audio.aac):
inputBuffers:
count = 46
input buffer #0:
timeUs = 1000001044000
contents = length 23, hash 47DE9131
input buffer #1:
timeUs = 1000001067219
contents = length 6, hash 31EC5206
input buffer #2:
timeUs = 1000001090439
contents = length 148, hash 894A176B
input buffer #3:
timeUs = 1000001113659
contents = length 189, hash CEF235A1
input buffer #4:
timeUs = 1000001136879
contents = length 205, hash BBF5F7B0
input buffer #5:
timeUs = 1000001160099
contents = length 210, hash F278B193
input buffer #6:
timeUs = 1000001183319
contents = length 210, hash 82DA1589
input buffer #7:
timeUs = 1000001206539
contents = length 207, hash 5BE231DF
input buffer #8:
timeUs = 1000001229759
contents = length 225, hash 18819EE1
input buffer #9:
timeUs = 1000001252979
contents = length 215, hash CA7FA67B
input buffer #10:
timeUs = 1000001276199
contents = length 211, hash 581A1C18
input buffer #11:
timeUs = 1000001299419
contents = length 216, hash ADB88187
input buffer #12:
timeUs = 1000001322639
contents = length 229, hash 2E8BA4DC
input buffer #13:
timeUs = 1000001345859
contents = length 232, hash 22F0C510
input buffer #14:
timeUs = 1000001369079
contents = length 235, hash 867AD0DC
input buffer #15:
timeUs = 1000001392299
contents = length 231, hash 84E823A8
input buffer #16:
timeUs = 1000001415519
contents = length 226, hash 1BEF3A95
input buffer #17:
timeUs = 1000001438739
contents = length 216, hash EAA345AE
input buffer #18:
timeUs = 1000001461959
contents = length 229, hash 6957411F
input buffer #19:
timeUs = 1000001485179
contents = length 219, hash 41275022
input buffer #20:
timeUs = 1000001508399
contents = length 241, hash 6495DF96
input buffer #21:
timeUs = 1000001531619
contents = length 228, hash 63D95906
input buffer #22:
timeUs = 1000001554839
contents = length 238, hash 34F676F9
input buffer #23:
timeUs = 1000001578058
contents = length 234, hash E5CBC045
input buffer #24:
timeUs = 1000001601278
contents = length 231, hash 5FC43661
input buffer #25:
timeUs = 1000001624498
contents = length 217, hash 682708ED
input buffer #26:
timeUs = 1000001647718
contents = length 239, hash D43780FC
input buffer #27:
timeUs = 1000001670938
contents = length 243, hash C5E17980
input buffer #28:
timeUs = 1000001694158
contents = length 231, hash AC5837BA
input buffer #29:
timeUs = 1000001717378
contents = length 230, hash 169EE895
input buffer #30:
timeUs = 1000001740598
contents = length 238, hash C48FF3F1
input buffer #31:
timeUs = 1000001763818
contents = length 225, hash 531E4599
input buffer #32:
timeUs = 1000001787038
contents = length 232, hash CB3C6B8D
input buffer #33:
timeUs = 1000001810258
contents = length 243, hash F8C94C7
input buffer #34:
timeUs = 1000001833478
contents = length 232, hash A646A7D0
input buffer #35:
timeUs = 1000001856698
contents = length 237, hash E8B787A5
input buffer #36:
timeUs = 1000001879918
contents = length 228, hash 3FA7A29F
input buffer #37:
timeUs = 1000001903138
contents = length 235, hash B9B33B0A
input buffer #38:
timeUs = 1000001926358
contents = length 264, hash 71A4869E
input buffer #39:
timeUs = 1000001949578
contents = length 257, hash D049B54C
input buffer #40:
timeUs = 1000001972798
contents = length 227, hash 66757231
input buffer #41:
timeUs = 1000001996018
contents = length 227, hash BD374F1B
input buffer #42:
timeUs = 1000002019238
contents = length 235, hash 999477F6
input buffer #43:
timeUs = 1000002042458
contents = length 229, hash FFF98DF0
input buffer #44:
timeUs = 1000002065678
contents = length 6, hash 31B22286
input buffer #45:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 45
output buffer #0:
timeUs = 1000001044000
size = 0
rendered = false
output buffer #1:
timeUs = 1000001067219
size = 0
rendered = false
output buffer #2:
timeUs = 1000001090439
size = 0
rendered = false
output buffer #3:
timeUs = 1000001113659
size = 0
rendered = false
output buffer #4:
timeUs = 1000001136879
size = 0
rendered = false
output buffer #5:
timeUs = 1000001160099
size = 0
rendered = false
output buffer #6:
timeUs = 1000001183319
size = 0
rendered = false
output buffer #7:
timeUs = 1000001206539
size = 0
rendered = false
output buffer #8:
timeUs = 1000001229759
size = 0
rendered = false
output buffer #9:
timeUs = 1000001252979
size = 0
rendered = false
output buffer #10:
timeUs = 1000001276199
size = 0
rendered = false
output buffer #11:
timeUs = 1000001299419
size = 0
rendered = false
output buffer #12:
timeUs = 1000001322639
size = 0
rendered = false
output buffer #13:
timeUs = 1000001345859
size = 0
rendered = false
output buffer #14:
timeUs = 1000001369079
size = 0
rendered = false
output buffer #15:
timeUs = 1000001392299
size = 0
rendered = false
output buffer #16:
timeUs = 1000001415519
size = 0
rendered = false
output buffer #17:
timeUs = 1000001438739
size = 0
rendered = false
output buffer #18:
timeUs = 1000001461959
size = 0
rendered = false
output buffer #19:
timeUs = 1000001485179
size = 0
rendered = false
output buffer #20:
timeUs = 1000001508399
size = 0
rendered = false
output buffer #21:
timeUs = 1000001531619
size = 0
rendered = false
output buffer #22:
timeUs = 1000001554839
size = 0
rendered = false
output buffer #23:
timeUs = 1000001578058
size = 0
rendered = false
output buffer #24:
timeUs = 1000001601278
size = 0
rendered = false
output buffer #25:
timeUs = 1000001624498
size = 0
rendered = false
output buffer #26:
timeUs = 1000001647718
size = 0
rendered = false
output buffer #27:
timeUs = 1000001670938
size = 0
rendered = false
output buffer #28:
timeUs = 1000001694158
size = 0
rendered = false
output buffer #29:
timeUs = 1000001717378
size = 0
rendered = false
output buffer #30:
timeUs = 1000001740598
size = 0
rendered = false
output buffer #31:
timeUs = 1000001763818
size = 0
rendered = false
output buffer #32:
timeUs = 1000001787038
size = 0
rendered = false
output buffer #33:
timeUs = 1000001810258
size = 0
rendered = false
output buffer #34:
timeUs = 1000001833478
size = 0
rendered = false
output buffer #35:
timeUs = 1000001856698
size = 0
rendered = false
output buffer #36:
timeUs = 1000001879918
size = 0
rendered = false
output buffer #37:
timeUs = 1000001903138
size = 0
rendered = false
output buffer #38:
timeUs = 1000001926358
size = 0
rendered = false
output buffer #39:
timeUs = 1000001949578
size = 0
rendered = false
output buffer #40:
timeUs = 1000001972798
size = 0
rendered = false
output buffer #41:
timeUs = 1000001996018
size = 0
rendered = false
output buffer #42:
timeUs = 1000002019238
size = 0
rendered = false
output buffer #43:
timeUs = 1000002042458
size = 0
rendered = false
output buffer #44:
timeUs = 1000002065678
size = 0
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000001000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000001066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000001033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000001200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000001133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000001100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000001166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000001333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000001266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000001233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000001300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000001433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000001400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000001367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000001567233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000001500500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000001467133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000001533866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000001700700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000001633966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000001600600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000001667333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000001834166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000001767433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000001734066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000001800800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000001967633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000001900900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000001867533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000001934266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000001000000
size = 36692
rendered = true
output buffer #1:
timeUs = 1000001066733
size = 5312
rendered = true
output buffer #2:
timeUs = 1000001033366
size = 599
rendered = true
output buffer #3:
timeUs = 1000001200200
size = 7735
rendered = true
output buffer #4:
timeUs = 1000001133466
size = 987
rendered = true
output buffer #5:
timeUs = 1000001100100
size = 673
rendered = true
output buffer #6:
timeUs = 1000001166833
size = 523
rendered = true
output buffer #7:
timeUs = 1000001333666
size = 6061
rendered = true
output buffer #8:
timeUs = 1000001266933
size = 992
rendered = true
output buffer #9:
timeUs = 1000001233566
size = 623
rendered = true
output buffer #10:
timeUs = 1000001300300
size = 421
rendered = true
output buffer #11:
timeUs = 1000001433766
size = 4899
rendered = true
output buffer #12:
timeUs = 1000001400400
size = 568
rendered = true
output buffer #13:
timeUs = 1000001367033
size = 620
rendered = true
output buffer #14:
timeUs = 1000001567233
size = 5450
rendered = true
output buffer #15:
timeUs = 1000001500500
size = 1051
rendered = true
output buffer #16:
timeUs = 1000001467133
size = 874
rendered = true
output buffer #17:
timeUs = 1000001533866
size = 781
rendered = true
output buffer #18:
timeUs = 1000001700700
size = 4725
rendered = true
output buffer #19:
timeUs = 1000001633966
size = 1022
rendered = true
output buffer #20:
timeUs = 1000001600600
size = 790
rendered = true
output buffer #21:
timeUs = 1000001667333
size = 610
rendered = true
output buffer #22:
timeUs = 1000001834166
size = 2751
rendered = true
output buffer #23:
timeUs = 1000001767433
size = 745
rendered = true
output buffer #24:
timeUs = 1000001734066
size = 621
rendered = true
output buffer #25:
timeUs = 1000001800800
size = 505
rendered = true
output buffer #26:
timeUs = 1000001967633
size = 1268
rendered = true
output buffer #27:
timeUs = 1000001900900
size = 880
rendered = true
output buffer #28:
timeUs = 1000001867533
size = 530
rendered = true
output buffer #29:
timeUs = 1000001934266
size = 568
rendered = true
AudioSink:
buffer count = 45
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #0:
time = 1000001044000
data = 1
buffer #1:
time = 1000001067219
data = 1
buffer #2:
time = 1000001090439
data = 1
buffer #3:
time = 1000001113659
data = 1
buffer #4:
time = 1000001136879
data = 1
buffer #5:
time = 1000001160099
data = 1
buffer #6:
time = 1000001183319
data = 1
buffer #7:
time = 1000001206539
data = 1
buffer #8:
time = 1000001229759
data = 1
buffer #9:
time = 1000001252979
data = 1
buffer #10:
time = 1000001276199
data = 1
buffer #11:
time = 1000001299419
data = 1
buffer #12:
time = 1000001322639
data = 1
buffer #13:
time = 1000001345859
data = 1
buffer #14:
time = 1000001369079
data = 1
buffer #15:
time = 1000001392299
data = 1
buffer #16:
time = 1000001415519
data = 1
buffer #17:
time = 1000001438739
data = 1
buffer #18:
time = 1000001461959
data = 1
buffer #19:
time = 1000001485179
data = 1
buffer #20:
time = 1000001508399
data = 1
buffer #21:
time = 1000001531619
data = 1
buffer #22:
time = 1000001554839
data = 1
buffer #23:
time = 1000001578058
data = 1
buffer #24:
time = 1000001601278
data = 1
buffer #25:
time = 1000001624498
data = 1
buffer #26:
time = 1000001647718
data = 1
buffer #27:
time = 1000001670938
data = 1
buffer #28:
time = 1000001694158
data = 1
buffer #29:
time = 1000001717378
data = 1
buffer #30:
time = 1000001740598
data = 1
buffer #31:
time = 1000001763818
data = 1
buffer #32:
time = 1000001787038
data = 1
buffer #33:
time = 1000001810258
data = 1
buffer #34:
time = 1000001833478
data = 1
buffer #35:
time = 1000001856698
data = 1
buffer #36:
time = 1000001879918
data = 1
buffer #37:
time = 1000001903138
data = 1
buffer #38:
time = 1000001926358
data = 1
buffer #39:
time = 1000001949578
data = 1
buffer #40:
time = 1000001972798
data = 1
buffer #41:
time = 1000001996018
data = 1
buffer #42:
time = 1000002019238
data = 1
buffer #43:
time = 1000002042458
data = 1
buffer #44:
time = 1000002065678
data = 1
ImageOutput:
rendered image count = 2
image output #1:
presentationTimeUs = 0
bitmap hash = -1041353260
image output #2:
presentationTimeUs = 0
bitmap hash = -1041353260

View File

@ -0,0 +1,895 @@
MediaCodecAdapter (exotest.audio.aac):
inputBuffers:
count = 48
input buffer #0:
timeUs = 1000000044000
contents = length 23, hash 47DE9131
input buffer #1:
timeUs = 1000000067219
contents = length 6, hash 31EC5206
input buffer #2:
timeUs = 1000000090439
contents = length 148, hash 894A176B
input buffer #3:
timeUs = 1000000113659
contents = length 189, hash CEF235A1
input buffer #4:
timeUs = 1000000136879
contents = length 205, hash BBF5F7B0
input buffer #5:
timeUs = 1000000160099
contents = length 210, hash F278B193
input buffer #6:
timeUs = 1000000183319
contents = length 210, hash 82DA1589
input buffer #7:
timeUs = 1000000206539
contents = length 207, hash 5BE231DF
input buffer #8:
timeUs = 1000000229759
contents = length 225, hash 18819EE1
input buffer #9:
timeUs = 0
flags = 4
contents = length 0, hash 1
input buffer #10:
timeUs = 1000000252979
contents = length 215, hash CA7FA67B
input buffer #11:
timeUs = 1000000276199
contents = length 211, hash 581A1C18
input buffer #12:
timeUs = 1000000299419
contents = length 216, hash ADB88187
input buffer #13:
timeUs = 1000000322639
contents = length 229, hash 2E8BA4DC
input buffer #14:
timeUs = 1000000345859
contents = length 232, hash 22F0C510
input buffer #15:
timeUs = 1000000369079
contents = length 235, hash 867AD0DC
input buffer #16:
timeUs = 1000000392299
contents = length 231, hash 84E823A8
input buffer #17:
timeUs = 1000000415519
contents = length 226, hash 1BEF3A95
input buffer #18:
timeUs = 1000000438739
contents = length 216, hash EAA345AE
input buffer #19:
timeUs = 1000000461959
contents = length 229, hash 6957411F
input buffer #20:
timeUs = 1000000485179
contents = length 219, hash 41275022
input buffer #21:
timeUs = 1000000508399
contents = length 241, hash 6495DF96
input buffer #22:
timeUs = 1000000531619
contents = length 228, hash 63D95906
input buffer #23:
timeUs = 1000000554839
contents = length 238, hash 34F676F9
input buffer #24:
timeUs = 1000000578058
contents = length 234, hash E5CBC045
input buffer #25:
timeUs = 0
flags = 4
contents = length 0, hash 1
input buffer #26:
timeUs = 1000000601278
contents = length 231, hash 5FC43661
input buffer #27:
timeUs = 1000000624498
contents = length 217, hash 682708ED
input buffer #28:
timeUs = 1000000647718
contents = length 239, hash D43780FC
input buffer #29:
timeUs = 1000000670938
contents = length 243, hash C5E17980
input buffer #30:
timeUs = 1000000694158
contents = length 231, hash AC5837BA
input buffer #31:
timeUs = 1000000717378
contents = length 230, hash 169EE895
input buffer #32:
timeUs = 1000000740598
contents = length 238, hash C48FF3F1
input buffer #33:
timeUs = 1000000763818
contents = length 225, hash 531E4599
input buffer #34:
timeUs = 1000000787038
contents = length 232, hash CB3C6B8D
input buffer #35:
timeUs = 1000000810258
contents = length 243, hash F8C94C7
input buffer #36:
timeUs = 1000000833478
contents = length 232, hash A646A7D0
input buffer #37:
timeUs = 1000000856698
contents = length 237, hash E8B787A5
input buffer #38:
timeUs = 1000000879918
contents = length 228, hash 3FA7A29F
input buffer #39:
timeUs = 1000000903138
contents = length 235, hash B9B33B0A
input buffer #40:
timeUs = 1000000926358
contents = length 264, hash 71A4869E
input buffer #41:
timeUs = 1000000949578
contents = length 257, hash D049B54C
input buffer #42:
timeUs = 1000000972798
contents = length 227, hash 66757231
input buffer #43:
timeUs = 1000000996018
contents = length 227, hash BD374F1B
input buffer #44:
timeUs = 1000001019238
contents = length 235, hash 999477F6
input buffer #45:
timeUs = 1000001042458
contents = length 229, hash FFF98DF0
input buffer #46:
timeUs = 1000001065678
contents = length 6, hash 31B22286
input buffer #47:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 45
output buffer #0:
timeUs = 1000000044000
size = 0
rendered = false
output buffer #1:
timeUs = 1000000067219
size = 0
rendered = false
output buffer #2:
timeUs = 1000000090439
size = 0
rendered = false
output buffer #3:
timeUs = 1000000113659
size = 0
rendered = false
output buffer #4:
timeUs = 1000000136879
size = 0
rendered = false
output buffer #5:
timeUs = 1000000160099
size = 0
rendered = false
output buffer #6:
timeUs = 1000000183319
size = 0
rendered = false
output buffer #7:
timeUs = 1000000206539
size = 0
rendered = false
output buffer #8:
timeUs = 1000000229759
size = 0
rendered = false
output buffer #9:
timeUs = 1000000252979
size = 0
rendered = false
output buffer #10:
timeUs = 1000000276199
size = 0
rendered = false
output buffer #11:
timeUs = 1000000299419
size = 0
rendered = false
output buffer #12:
timeUs = 1000000322639
size = 0
rendered = false
output buffer #13:
timeUs = 1000000345859
size = 0
rendered = false
output buffer #14:
timeUs = 1000000369079
size = 0
rendered = false
output buffer #15:
timeUs = 1000000392299
size = 0
rendered = false
output buffer #16:
timeUs = 1000000415519
size = 0
rendered = false
output buffer #17:
timeUs = 1000000438739
size = 0
rendered = false
output buffer #18:
timeUs = 1000000461959
size = 0
rendered = false
output buffer #19:
timeUs = 1000000485179
size = 0
rendered = false
output buffer #20:
timeUs = 1000000508399
size = 0
rendered = false
output buffer #21:
timeUs = 1000000531619
size = 0
rendered = false
output buffer #22:
timeUs = 1000000554839
size = 0
rendered = false
output buffer #23:
timeUs = 1000000578058
size = 0
rendered = false
output buffer #24:
timeUs = 1000000601278
size = 0
rendered = false
output buffer #25:
timeUs = 1000000624498
size = 0
rendered = false
output buffer #26:
timeUs = 1000000647718
size = 0
rendered = false
output buffer #27:
timeUs = 1000000670938
size = 0
rendered = false
output buffer #28:
timeUs = 1000000694158
size = 0
rendered = false
output buffer #29:
timeUs = 1000000717378
size = 0
rendered = false
output buffer #30:
timeUs = 1000000740598
size = 0
rendered = false
output buffer #31:
timeUs = 1000000763818
size = 0
rendered = false
output buffer #32:
timeUs = 1000000787038
size = 0
rendered = false
output buffer #33:
timeUs = 1000000810258
size = 0
rendered = false
output buffer #34:
timeUs = 1000000833478
size = 0
rendered = false
output buffer #35:
timeUs = 1000000856698
size = 0
rendered = false
output buffer #36:
timeUs = 1000000879918
size = 0
rendered = false
output buffer #37:
timeUs = 1000000903138
size = 0
rendered = false
output buffer #38:
timeUs = 1000000926358
size = 0
rendered = false
output buffer #39:
timeUs = 1000000949578
size = 0
rendered = false
output buffer #40:
timeUs = 1000000972798
size = 0
rendered = false
output buffer #41:
timeUs = 1000000996018
size = 0
rendered = false
output buffer #42:
timeUs = 1000001019238
size = 0
rendered = false
output buffer #43:
timeUs = 1000001042458
size = 0
rendered = false
output buffer #44:
timeUs = 1000001065678
size = 0
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 8
input buffer #0:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 7
output buffer #0:
timeUs = 1000000000000
size = 36692
rendered = true
output buffer #1:
timeUs = 1000000066733
size = 5312
rendered = true
output buffer #2:
timeUs = 1000000033366
size = 599
rendered = true
output buffer #3:
timeUs = 1000000200200
size = 7735
rendered = true
output buffer #4:
timeUs = 1000000133466
size = 987
rendered = true
output buffer #5:
timeUs = 1000000100100
size = 673
rendered = true
output buffer #6:
timeUs = 1000000166833
size = 523
rendered = true
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 19
input buffer #0:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000000333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000000266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000000233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000000300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000000433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000000400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000000367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000000567233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000000500500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000000467133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000000533866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 18
output buffer #0:
timeUs = 1000000000000
size = 36692
rendered = false
output buffer #1:
timeUs = 1000000066733
size = 5312
rendered = false
output buffer #2:
timeUs = 1000000033366
size = 599
rendered = false
output buffer #3:
timeUs = 1000000200200
size = 7735
rendered = false
output buffer #4:
timeUs = 1000000133466
size = 987
rendered = false
output buffer #5:
timeUs = 1000000100100
size = 673
rendered = false
output buffer #6:
timeUs = 1000000166833
size = 523
rendered = false
output buffer #7:
timeUs = 1000000333666
size = 6061
rendered = false
output buffer #8:
timeUs = 1000000266933
size = 992
rendered = false
output buffer #9:
timeUs = 1000000233566
size = 623
rendered = false
output buffer #10:
timeUs = 1000000300300
size = 421
rendered = false
output buffer #11:
timeUs = 1000000433766
size = 4899
rendered = false
output buffer #12:
timeUs = 1000000400400
size = 568
rendered = false
output buffer #13:
timeUs = 1000000367033
size = 620
rendered = false
output buffer #14:
timeUs = 1000000567233
size = 5450
rendered = false
output buffer #15:
timeUs = 1000000500500
size = 1051
rendered = false
output buffer #16:
timeUs = 1000000467133
size = 874
rendered = false
output buffer #17:
timeUs = 1000000533866
size = 781
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000000333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000000266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000000233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000000300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000000433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000000400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000000367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000000567233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000000500500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000000467133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000000533866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000000700700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000000633966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000000600600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000000667333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000000834166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000000767433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000000734066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000000800800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000000967633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000000900900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000000867533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000000934266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000000000000
size = 36692
rendered = false
output buffer #1:
timeUs = 1000000066733
size = 5312
rendered = false
output buffer #2:
timeUs = 1000000033366
size = 599
rendered = false
output buffer #3:
timeUs = 1000000200200
size = 7735
rendered = false
output buffer #4:
timeUs = 1000000133466
size = 987
rendered = false
output buffer #5:
timeUs = 1000000100100
size = 673
rendered = false
output buffer #6:
timeUs = 1000000166833
size = 523
rendered = false
output buffer #7:
timeUs = 1000000333666
size = 6061
rendered = false
output buffer #8:
timeUs = 1000000266933
size = 992
rendered = false
output buffer #9:
timeUs = 1000000233566
size = 623
rendered = false
output buffer #10:
timeUs = 1000000300300
size = 421
rendered = false
output buffer #11:
timeUs = 1000000433766
size = 4899
rendered = false
output buffer #12:
timeUs = 1000000400400
size = 568
rendered = false
output buffer #13:
timeUs = 1000000367033
size = 620
rendered = false
output buffer #14:
timeUs = 1000000567233
size = 5450
rendered = false
output buffer #15:
timeUs = 1000000500500
size = 1051
rendered = false
output buffer #16:
timeUs = 1000000467133
size = 874
rendered = false
output buffer #17:
timeUs = 1000000533866
size = 781
rendered = false
output buffer #18:
timeUs = 1000000700700
size = 4725
rendered = true
output buffer #19:
timeUs = 1000000633966
size = 1022
rendered = true
output buffer #20:
timeUs = 1000000600600
size = 790
rendered = true
output buffer #21:
timeUs = 1000000667333
size = 610
rendered = true
output buffer #22:
timeUs = 1000000834166
size = 2751
rendered = true
output buffer #23:
timeUs = 1000000767433
size = 745
rendered = true
output buffer #24:
timeUs = 1000000734066
size = 621
rendered = true
output buffer #25:
timeUs = 1000000800800
size = 505
rendered = true
output buffer #26:
timeUs = 1000000967633
size = 1268
rendered = true
output buffer #27:
timeUs = 1000000900900
size = 880
rendered = true
output buffer #28:
timeUs = 1000000867533
size = 530
rendered = true
output buffer #29:
timeUs = 1000000934266
size = 568
rendered = true
AudioSink:
buffer count = 45
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #0:
time = 1000000044000
data = 1
buffer #1:
time = 1000000067219
data = 1
buffer #2:
time = 1000000090439
data = 1
buffer #3:
time = 1000000113659
data = 1
buffer #4:
time = 1000000136879
data = 1
buffer #5:
time = 1000000160099
data = 1
buffer #6:
time = 1000000183319
data = 1
buffer #7:
time = 1000000206539
data = 1
buffer #8:
time = 1000000229759
data = 1
discontinuity:
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #9:
time = 1000000252979
data = 1
buffer #10:
time = 1000000276199
data = 1
buffer #11:
time = 1000000299419
data = 1
buffer #12:
time = 1000000322639
data = 1
buffer #13:
time = 1000000345859
data = 1
buffer #14:
time = 1000000369079
data = 1
buffer #15:
time = 1000000392299
data = 1
buffer #16:
time = 1000000415519
data = 1
buffer #17:
time = 1000000438739
data = 1
buffer #18:
time = 1000000461959
data = 1
buffer #19:
time = 1000000485179
data = 1
buffer #20:
time = 1000000508399
data = 1
buffer #21:
time = 1000000531619
data = 1
buffer #22:
time = 1000000554839
data = 1
buffer #23:
time = 1000000578058
data = 1
discontinuity:
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #24:
time = 1000000601278
data = 1
buffer #25:
time = 1000000624498
data = 1
buffer #26:
time = 1000000647718
data = 1
buffer #27:
time = 1000000670938
data = 1
buffer #28:
time = 1000000694158
data = 1
buffer #29:
time = 1000000717378
data = 1
buffer #30:
time = 1000000740598
data = 1
buffer #31:
time = 1000000763818
data = 1
buffer #32:
time = 1000000787038
data = 1
buffer #33:
time = 1000000810258
data = 1
buffer #34:
time = 1000000833478
data = 1
buffer #35:
time = 1000000856698
data = 1
buffer #36:
time = 1000000879918
data = 1
buffer #37:
time = 1000000903138
data = 1
buffer #38:
time = 1000000926358
data = 1
buffer #39:
time = 1000000949578
data = 1
buffer #40:
time = 1000000972798
data = 1
buffer #41:
time = 1000000996018
data = 1
buffer #42:
time = 1000001019238
data = 1
buffer #43:
time = 1000001042458
data = 1
buffer #44:
time = 1000001065678
data = 1

View File

@ -0,0 +1,657 @@
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000000000000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000066733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000033366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000200200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000133466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000100100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000166833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000000333666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000000266933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000000233566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000000300300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000000433766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000000400400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000000367033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000000567233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000000500500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000000467133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000000533866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000000700700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000000633966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000000600600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000000667333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000000834166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000000767433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000000734066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000000800800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000000967633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000000900900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000000867533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000000934266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000000000000
size = 36692
rendered = true
output buffer #1:
timeUs = 1000000066733
size = 5312
rendered = true
output buffer #2:
timeUs = 1000000033366
size = 599
rendered = true
output buffer #3:
timeUs = 1000000200200
size = 7735
rendered = true
output buffer #4:
timeUs = 1000000133466
size = 987
rendered = true
output buffer #5:
timeUs = 1000000100100
size = 673
rendered = true
output buffer #6:
timeUs = 1000000166833
size = 523
rendered = true
output buffer #7:
timeUs = 1000000333666
size = 6061
rendered = true
output buffer #8:
timeUs = 1000000266933
size = 992
rendered = true
output buffer #9:
timeUs = 1000000233566
size = 623
rendered = true
output buffer #10:
timeUs = 1000000300300
size = 421
rendered = true
output buffer #11:
timeUs = 1000000433766
size = 4899
rendered = true
output buffer #12:
timeUs = 1000000400400
size = 568
rendered = true
output buffer #13:
timeUs = 1000000367033
size = 620
rendered = true
output buffer #14:
timeUs = 1000000567233
size = 5450
rendered = true
output buffer #15:
timeUs = 1000000500500
size = 1051
rendered = true
output buffer #16:
timeUs = 1000000467133
size = 874
rendered = true
output buffer #17:
timeUs = 1000000533866
size = 781
rendered = true
output buffer #18:
timeUs = 1000000700700
size = 4725
rendered = true
output buffer #19:
timeUs = 1000000633966
size = 1022
rendered = true
output buffer #20:
timeUs = 1000000600600
size = 790
rendered = true
output buffer #21:
timeUs = 1000000667333
size = 610
rendered = true
output buffer #22:
timeUs = 1000000834166
size = 2751
rendered = true
output buffer #23:
timeUs = 1000000767433
size = 745
rendered = true
output buffer #24:
timeUs = 1000000734066
size = 621
rendered = true
output buffer #25:
timeUs = 1000000800800
size = 505
rendered = true
output buffer #26:
timeUs = 1000000967633
size = 1268
rendered = true
output buffer #27:
timeUs = 1000000900900
size = 880
rendered = true
output buffer #28:
timeUs = 1000000867533
size = 530
rendered = true
output buffer #29:
timeUs = 1000000934266
size = 568
rendered = true
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000001024000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000001090733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000001057366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000001224200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000001157466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000001124100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000001190833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000001357666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000001290933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000001257566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000001324300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000001457766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000001424400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000001391033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000001591233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000001524500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000001491133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000001557866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000001724700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000001657966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000001624600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000001691333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000001858166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000001791433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000001758066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000001824800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000001991633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000001924900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000001891533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000001958266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000001024000
size = 36692
rendered = false
output buffer #1:
timeUs = 1000001090733
size = 5312
rendered = false
output buffer #2:
timeUs = 1000001057366
size = 599
rendered = false
output buffer #3:
timeUs = 1000001224200
size = 7735
rendered = false
output buffer #4:
timeUs = 1000001157466
size = 987
rendered = false
output buffer #5:
timeUs = 1000001124100
size = 673
rendered = false
output buffer #6:
timeUs = 1000001190833
size = 523
rendered = false
output buffer #7:
timeUs = 1000001357666
size = 6061
rendered = false
output buffer #8:
timeUs = 1000001290933
size = 992
rendered = false
output buffer #9:
timeUs = 1000001257566
size = 623
rendered = false
output buffer #10:
timeUs = 1000001324300
size = 421
rendered = false
output buffer #11:
timeUs = 1000001457766
size = 4899
rendered = false
output buffer #12:
timeUs = 1000001424400
size = 568
rendered = false
output buffer #13:
timeUs = 1000001391033
size = 620
rendered = false
output buffer #14:
timeUs = 1000001591233
size = 5450
rendered = false
output buffer #15:
timeUs = 1000001524500
size = 1051
rendered = false
output buffer #16:
timeUs = 1000001491133
size = 874
rendered = false
output buffer #17:
timeUs = 1000001557866
size = 781
rendered = false
output buffer #18:
timeUs = 1000001724700
size = 4725
rendered = false
output buffer #19:
timeUs = 1000001657966
size = 1022
rendered = false
output buffer #20:
timeUs = 1000001624600
size = 790
rendered = false
output buffer #21:
timeUs = 1000001691333
size = 610
rendered = false
output buffer #22:
timeUs = 1000001858166
size = 2751
rendered = false
output buffer #23:
timeUs = 1000001791433
size = 745
rendered = false
output buffer #24:
timeUs = 1000001758066
size = 621
rendered = false
output buffer #25:
timeUs = 1000001824800
size = 505
rendered = false
output buffer #26:
timeUs = 1000001991633
size = 1268
rendered = false
output buffer #27:
timeUs = 1000001924900
size = 880
rendered = false
output buffer #28:
timeUs = 1000001891533
size = 530
rendered = false
output buffer #29:
timeUs = 1000001958266
size = 568
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 31
input buffer #0:
timeUs = 1000002048000
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000002114733
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000002081366
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000002248200
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000002181466
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000002148100
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000002214833
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000002381666
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000002314933
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000002281566
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000002348300
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000002481766
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000002448400
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000002415033
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000002615233
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000002548500
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000002515133
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000002581866
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 1000002748700
contents = length 4725, hash AC0C8CD3
input buffer #19:
timeUs = 1000002681966
contents = length 1022, hash 5D8BFF34
input buffer #20:
timeUs = 1000002648600
contents = length 790, hash 99413A99
input buffer #21:
timeUs = 1000002715333
contents = length 610, hash 5E129290
input buffer #22:
timeUs = 1000002882166
contents = length 2751, hash 769974CB
input buffer #23:
timeUs = 1000002815433
contents = length 745, hash B78A477A
input buffer #24:
timeUs = 1000002782066
contents = length 621, hash CF741E7A
input buffer #25:
timeUs = 1000002848800
contents = length 505, hash 1DB4894E
input buffer #26:
timeUs = 1000003015633
contents = length 1268, hash C15348DC
input buffer #27:
timeUs = 1000002948900
contents = length 880, hash C2DE85D0
input buffer #28:
timeUs = 1000002915533
contents = length 530, hash C98BC6A8
input buffer #29:
timeUs = 1000002982266
contents = length 568, hash 4FE5C8EA
input buffer #30:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 30
output buffer #0:
timeUs = 1000002048000
size = 36692
rendered = true
output buffer #1:
timeUs = 1000002114733
size = 5312
rendered = true
output buffer #2:
timeUs = 1000002081366
size = 599
rendered = true
output buffer #3:
timeUs = 1000002248200
size = 7735
rendered = true
output buffer #4:
timeUs = 1000002181466
size = 987
rendered = true
output buffer #5:
timeUs = 1000002148100
size = 673
rendered = true
output buffer #6:
timeUs = 1000002214833
size = 523
rendered = true
output buffer #7:
timeUs = 1000002381666
size = 6061
rendered = true
output buffer #8:
timeUs = 1000002314933
size = 992
rendered = true
output buffer #9:
timeUs = 1000002281566
size = 623
rendered = true
output buffer #10:
timeUs = 1000002348300
size = 421
rendered = true
output buffer #11:
timeUs = 1000002481766
size = 4899
rendered = true
output buffer #12:
timeUs = 1000002448400
size = 568
rendered = true
output buffer #13:
timeUs = 1000002415033
size = 620
rendered = true
output buffer #14:
timeUs = 1000002615233
size = 5450
rendered = true
output buffer #15:
timeUs = 1000002548500
size = 1051
rendered = true
output buffer #16:
timeUs = 1000002515133
size = 874
rendered = true
output buffer #17:
timeUs = 1000002581866
size = 781
rendered = true
output buffer #18:
timeUs = 1000002748700
size = 4725
rendered = true
output buffer #19:
timeUs = 1000002681966
size = 1022
rendered = true
output buffer #20:
timeUs = 1000002648600
size = 790
rendered = true
output buffer #21:
timeUs = 1000002715333
size = 610
rendered = true
output buffer #22:
timeUs = 1000002882166
size = 2751
rendered = true
output buffer #23:
timeUs = 1000002815433
size = 745
rendered = true
output buffer #24:
timeUs = 1000002782066
size = 621
rendered = true
output buffer #25:
timeUs = 1000002848800
size = 505
rendered = true
output buffer #26:
timeUs = 1000003015633
size = 1268
rendered = true
output buffer #27:
timeUs = 1000002948900
size = 880
rendered = true
output buffer #28:
timeUs = 1000002915533
size = 530
rendered = true
output buffer #29:
timeUs = 1000002982266
size = 568
rendered = true

View File

@ -0,0 +1,374 @@
MediaCodecAdapter (exotest.audio.aac):
inputBuffers:
count = 13
input buffer #0:
timeUs = 1000000310972
contents = length 229, hash 2E8BA4DC
input buffer #1:
timeUs = 1000000334192
contents = length 232, hash 22F0C510
input buffer #2:
timeUs = 1000000357412
contents = length 235, hash 867AD0DC
input buffer #3:
timeUs = 1000000380632
contents = length 231, hash 84E823A8
input buffer #4:
timeUs = 1000000403852
contents = length 226, hash 1BEF3A95
input buffer #5:
timeUs = 1000000427072
contents = length 216, hash EAA345AE
input buffer #6:
timeUs = 1000000450292
contents = length 229, hash 6957411F
input buffer #7:
timeUs = 1000000473512
contents = length 219, hash 41275022
input buffer #8:
timeUs = 1000000496732
contents = length 241, hash 6495DF96
input buffer #9:
timeUs = 1000000519952
contents = length 228, hash 63D95906
input buffer #10:
timeUs = 1000000543172
contents = length 238, hash 34F676F9
input buffer #11:
timeUs = 1000000566391
contents = length 234, hash E5CBC045
input buffer #12:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 12
output buffer #0:
timeUs = 1000000310972
size = 0
rendered = false
output buffer #1:
timeUs = 1000000334192
size = 0
rendered = false
output buffer #2:
timeUs = 1000000357412
size = 0
rendered = false
output buffer #3:
timeUs = 1000000380632
size = 0
rendered = false
output buffer #4:
timeUs = 1000000403852
size = 0
rendered = false
output buffer #5:
timeUs = 1000000427072
size = 0
rendered = false
output buffer #6:
timeUs = 1000000450292
size = 0
rendered = false
output buffer #7:
timeUs = 1000000473512
size = 0
rendered = false
output buffer #8:
timeUs = 1000000496732
size = 0
rendered = false
output buffer #9:
timeUs = 1000000519952
size = 0
rendered = false
output buffer #10:
timeUs = 1000000543172
size = 0
rendered = false
output buffer #11:
timeUs = 1000000566391
size = 0
rendered = false
MediaCodecAdapter (exotest.audio.ac3):
inputBuffers:
count = 10
input buffer #0:
timeUs = 1000000000000
contents = length 1536, hash 7108D5C2
input buffer #1:
timeUs = 1000000032000
contents = length 1536, hash 80BF3B34
input buffer #2:
timeUs = 1000000064000
contents = length 1536, hash 5D09685
input buffer #3:
timeUs = 1000000096000
contents = length 1536, hash A9A24E44
input buffer #4:
timeUs = 1000000128000
contents = length 1536, hash 6F856273
input buffer #5:
timeUs = 1000000160000
contents = length 1536, hash B1737D3C
input buffer #6:
timeUs = 1000000192000
contents = length 1536, hash 98FDEB9D
input buffer #7:
timeUs = 1000000224000
contents = length 1536, hash 99B9B943
input buffer #8:
timeUs = 1000000256000
contents = length 1536, hash AAD9FCD2
input buffer #9:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 9
output buffer #0:
timeUs = 1000000000000
size = 0
rendered = false
output buffer #1:
timeUs = 1000000032000
size = 0
rendered = false
output buffer #2:
timeUs = 1000000064000
size = 0
rendered = false
output buffer #3:
timeUs = 1000000096000
size = 0
rendered = false
output buffer #4:
timeUs = 1000000128000
size = 0
rendered = false
output buffer #5:
timeUs = 1000000160000
size = 0
rendered = false
output buffer #6:
timeUs = 1000000192000
size = 0
rendered = false
output buffer #7:
timeUs = 1000000224000
size = 0
rendered = false
output buffer #8:
timeUs = 1000000256000
size = 0
rendered = false
MediaCodecAdapter (exotest.video.avc):
inputBuffers:
count = 19
input buffer #0:
timeUs = 999999988333
contents = length 36692, hash D216076E
input buffer #1:
timeUs = 1000000055066
contents = length 5312, hash D45D3CA0
input buffer #2:
timeUs = 1000000021699
contents = length 599, hash 1BE7812D
input buffer #3:
timeUs = 1000000188533
contents = length 7735, hash 4490F110
input buffer #4:
timeUs = 1000000121799
contents = length 987, hash 560B5036
input buffer #5:
timeUs = 1000000088433
contents = length 673, hash ED7CD8C7
input buffer #6:
timeUs = 1000000155166
contents = length 523, hash 3020DF50
input buffer #7:
timeUs = 1000000321999
contents = length 6061, hash 736C72B2
input buffer #8:
timeUs = 1000000255266
contents = length 992, hash FE132F23
input buffer #9:
timeUs = 1000000221899
contents = length 623, hash 5B2C1816
input buffer #10:
timeUs = 1000000288633
contents = length 421, hash 742E69C1
input buffer #11:
timeUs = 1000000422099
contents = length 4899, hash F72F86A1
input buffer #12:
timeUs = 1000000388733
contents = length 568, hash 519A8E50
input buffer #13:
timeUs = 1000000355366
contents = length 620, hash 3990AA39
input buffer #14:
timeUs = 1000000555566
contents = length 5450, hash F06EC4AA
input buffer #15:
timeUs = 1000000488833
contents = length 1051, hash 92DFA63A
input buffer #16:
timeUs = 1000000455466
contents = length 874, hash 69587FB4
input buffer #17:
timeUs = 1000000522199
contents = length 781, hash 36BE495B
input buffer #18:
timeUs = 0
flags = 4
contents = length 0, hash 1
outputBuffers:
count = 18
output buffer #0:
timeUs = 999999988333
size = 36692
rendered = false
output buffer #1:
timeUs = 1000000055066
size = 5312
rendered = false
output buffer #2:
timeUs = 1000000021699
size = 599
rendered = false
output buffer #3:
timeUs = 1000000188533
size = 7735
rendered = false
output buffer #4:
timeUs = 1000000121799
size = 987
rendered = false
output buffer #5:
timeUs = 1000000088433
size = 673
rendered = false
output buffer #6:
timeUs = 1000000155166
size = 523
rendered = false
output buffer #7:
timeUs = 1000000321999
size = 6061
rendered = true
output buffer #8:
timeUs = 1000000255266
size = 992
rendered = false
output buffer #9:
timeUs = 1000000221899
size = 623
rendered = false
output buffer #10:
timeUs = 1000000288633
size = 421
rendered = true
output buffer #11:
timeUs = 1000000422099
size = 4899
rendered = true
output buffer #12:
timeUs = 1000000388733
size = 568
rendered = true
output buffer #13:
timeUs = 1000000355366
size = 620
rendered = true
output buffer #14:
timeUs = 1000000555566
size = 5450
rendered = true
output buffer #15:
timeUs = 1000000488833
size = 1051
rendered = true
output buffer #16:
timeUs = 1000000455466
size = 874
rendered = true
output buffer #17:
timeUs = 1000000522199
size = 781
rendered = true
AudioSink:
buffer count = 21
config:
pcmEncoding = 2
channelCount = 6
sampleRate = 48000
buffer #0:
time = 1000000000000
data = 1
buffer #1:
time = 1000000032000
data = 1
buffer #2:
time = 1000000064000
data = 1
buffer #3:
time = 1000000096000
data = 1
buffer #4:
time = 1000000128000
data = 1
buffer #5:
time = 1000000160000
data = 1
buffer #6:
time = 1000000192000
data = 1
buffer #7:
time = 1000000224000
data = 1
buffer #8:
time = 1000000256000
data = 1
discontinuity:
config:
pcmEncoding = 2
channelCount = 1
sampleRate = 44100
buffer #9:
time = 1000000310972
data = 1
buffer #10:
time = 1000000334192
data = 1
buffer #11:
time = 1000000357412
data = 1
buffer #12:
time = 1000000380632
data = 1
buffer #13:
time = 1000000403852
data = 1
buffer #14:
time = 1000000427072
data = 1
buffer #15:
time = 1000000450292
data = 1
buffer #16:
time = 1000000473512
data = 1
buffer #17:
time = 1000000496732
data = 1
buffer #18:
time = 1000000519952
data = 1
buffer #19:
time = 1000000543172
data = 1
buffer #20:
time = 1000000566391
data = 1

View File

@ -126,7 +126,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
MetadataOutput metadataRendererOutput) { MetadataOutput metadataRendererOutput) {
ArrayList<Renderer> renderers = new ArrayList<>(); ArrayList<Renderer> renderers = new ArrayList<>();
renderers.add( renderers.add(
new MediaCodecVideoRenderer( new CapturingMediaCodecVideoRenderer(
context, context,
mediaCodecAdapterFactory, mediaCodecAdapterFactory,
MediaCodecSelector.DEFAULT, MediaCodecSelector.DEFAULT,
@ -134,27 +134,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
/* enableDecoderFallback= */ false, /* enableDecoderFallback= */ false,
eventHandler, eventHandler,
videoRendererEventListener, videoRendererEventListener,
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY) { DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
@Override
protected boolean shouldDropOutputBuffer(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
// Do not drop output buffers due to slow processing.
return false;
}
@Override
protected boolean shouldDropBuffersToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
// Do not drop output buffers due to slow processing.
return false;
}
@Override
protected boolean shouldSkipBuffersWithIdenticalReleaseTime() {
// Do not skip buffers with identical vsync times as we can't control this from tests.
return false;
}
});
renderers.add( renderers.add(
new MediaCodecAudioRenderer( new MediaCodecAudioRenderer(
context, context,
@ -190,6 +170,72 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
Renderer create(TextOutput textOutput, Looper outputLooper); Renderer create(TextOutput textOutput, Looper outputLooper);
} }
/**
* Returns new instance of a specialized {@link MediaCodecVideoRenderer} that will not drop or
* skip buffers due to slow processing.
*
* @param eventHandler A handler to use when invoking event listeners and outputs.
* @param videoRendererEventListener An event listener for video renderers.
* @return a new instance of a specialized {@link MediaCodecVideoRenderer}.
*/
protected MediaCodecVideoRenderer createMediaCodecVideoRenderer(
Handler eventHandler, VideoRendererEventListener videoRendererEventListener) {
return new CapturingMediaCodecVideoRenderer(
context,
mediaCodecAdapterFactory,
MediaCodecSelector.DEFAULT,
DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS,
/* enableDecoderFallback= */ false,
eventHandler,
videoRendererEventListener,
DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
}
/**
* A {@link MediaCodecVideoRenderer} that will not skip or drop buffers due to slow processing.
*/
private static class CapturingMediaCodecVideoRenderer extends MediaCodecVideoRenderer {
private CapturingMediaCodecVideoRenderer(
Context context,
MediaCodecAdapter.Factory codecAdapterFactory,
MediaCodecSelector mediaCodecSelector,
long allowedJoiningTimeMs,
boolean enableDecoderFallback,
@Nullable Handler eventHandler,
@Nullable VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
super(
context,
codecAdapterFactory,
mediaCodecSelector,
allowedJoiningTimeMs,
enableDecoderFallback,
eventHandler,
eventListener,
maxDroppedFramesToNotify);
}
@Override
protected boolean shouldDropOutputBuffer(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
// Do not drop output buffers due to slow processing.
return false;
}
@Override
protected boolean shouldDropBuffersToKeyframe(
long earlyUs, long elapsedRealtimeUs, boolean isLastBuffer) {
// Do not drop output buffers due to slow processing.
return false;
}
@Override
protected boolean shouldSkipBuffersWithIdenticalReleaseTime() {
// Do not skip buffers with identical vsync times as we can't control this from tests.
return false;
}
}
/** /**
* A {@link MediaCodecAdapter} that captures interactions and exposes them for test assertions via * A {@link MediaCodecAdapter} that captures interactions and exposes them for test assertions via
* {@link Dumper.Dumpable}. * {@link Dumper.Dumpable}.