/* * Copyright (C) 2018 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 java.lang.Math.max; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Log; import androidx.media3.common.util.NullableType; import androidx.media3.exoplayer.source.ClippingMediaPeriod; import androidx.media3.exoplayer.source.EmptySampleStream; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.SampleStream; import androidx.media3.exoplayer.source.TrackGroupArray; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.TrackSelector; import androidx.media3.exoplayer.trackselection.TrackSelectorResult; import androidx.media3.exoplayer.upstream.Allocator; /** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ /* package */ final class MediaPeriodHolder { private static final String TAG = "MediaPeriodHolder"; /** The {@link MediaPeriod} wrapped by this class. */ public final MediaPeriod mediaPeriod; /** The unique timeline period identifier the media period belongs to. */ public final Object uid; /** * The sample streams for each renderer associated with this period. May contain null elements. */ public final @NullableType SampleStream[] sampleStreams; /** Whether the media period has finished preparing. */ public boolean prepared; /** Whether any of the tracks of this media period are enabled. */ public boolean hasEnabledTracks; /** {@link MediaPeriodInfo} about this media period. */ public MediaPeriodInfo info; /** * Whether all renderers are in the correct state for this {@link #mediaPeriod}. * *

Renderers that are needed must have been enabled with the {@link #sampleStreams} for this * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[], * SampleStream, long, boolean, boolean, long, long)} or {@link Renderer#replaceStream(Format[], * SampleStream, long, long)} has been called. * *

Renderers that are not needed must have been {@link Renderer#disable() disabled}. */ public boolean allRenderersInCorrectState; private final boolean[] mayRetainStreamFlags; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; private final MediaSourceList mediaSourceList; @Nullable private MediaPeriodHolder next; private TrackGroupArray trackGroups; private TrackSelectorResult trackSelectorResult; private long rendererPositionOffsetUs; /** * Creates a new holder with information required to play it as part of a timeline. * * @param rendererCapabilities The renderer capabilities. * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. * @param trackSelector The track selector. * @param allocator The allocator. * @param mediaSourceList The playlist. * @param info Information used to identify this media period in its timeline period. * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each * renderer. */ public MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, MediaSourceList mediaSourceList, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult) { this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.mediaSourceList = mediaSourceList; this.uid = info.id.periodUid; this.info = info; this.trackGroups = TrackGroupArray.EMPTY; this.trackSelectorResult = emptyTrackSelectorResult; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; mediaPeriod = createMediaPeriod( info.id, mediaSourceList, allocator, info.startPositionUs, info.endPositionUs); } /** * Converts time relative to the start of the period to the respective renderer time using {@link * #getRendererOffset()}, in microseconds. */ public long toRendererTime(long periodTimeUs) { return periodTimeUs + getRendererOffset(); } /** * Converts renderer time to the respective time relative to the start of the period using {@link * #getRendererOffset()}, in microseconds. */ public long toPeriodTime(long rendererTimeUs) { return rendererTimeUs - getRendererOffset(); } /** Returns the renderer time of the start of the period, in microseconds. */ public long getRendererOffset() { return rendererPositionOffsetUs; } /** * Sets the renderer time of the start of the period, in microseconds. * * @param rendererPositionOffsetUs The new renderer position offset, in microseconds. */ public void setRendererOffset(long rendererPositionOffsetUs) { this.rendererPositionOffsetUs = rendererPositionOffsetUs; } /** Returns start position of period in renderer time. */ public long getStartPositionRendererTime() { return info.startPositionUs + rendererPositionOffsetUs; } /** Returns whether the period is fully buffered. */ public boolean isFullyBuffered() { return prepared && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } /** * Returns the buffered position in microseconds. If the period is buffered to the end, then the * period duration is returned. * * @return The buffered position in microseconds. */ public long getBufferedPositionUs() { if (!prepared) { return info.startPositionUs; } long bufferedPositionUs = hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE; return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs; } /** * Returns the next load time relative to the start of the period, or {@link C#TIME_END_OF_SOURCE} * if loading has finished. */ public long getNextLoadPositionUs() { return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); } /** * Handles period preparation. * * @param playbackSpeed The current factor by which playback is sped up. * @param timeline The current {@link Timeline}. * @throws ExoPlaybackException If an error occurs during track selection. */ public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); long requestedStartPositionUs = info.startPositionUs; if (info.durationUs != C.TIME_UNSET && requestedStartPositionUs >= info.durationUs) { // Make sure start position doesn't exceed period duration. requestedStartPositionUs = max(0, info.durationUs - 1); } long newStartPositionUs = applyTrackSelection( selectorResult, requestedStartPositionUs, /* forceRecreateStreams= */ false); rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); } /** * Reevaluates the buffer of the media period at the given renderer position. Should only be * called if this is the loading media period. * * @param rendererPositionUs The playing position in renderer time, in microseconds. */ public void reevaluateBuffer(long rendererPositionUs) { Assertions.checkState(isLoadingMediaPeriod()); if (prepared) { mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs)); } } /** * Continues loading the media period at the given renderer position. Should only be called if * this is the loading media period. * * @param rendererPositionUs The load position in renderer time, in microseconds. */ public void continueLoading(long rendererPositionUs) { Assertions.checkState(isLoadingMediaPeriod()); long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); mediaPeriod.continueLoading(loadingPeriodPositionUs); } /** * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. * *

The new track selection needs to be applied with {@link * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. * * @param playbackSpeed The current factor by which playback is sped up. * @param timeline The current {@link Timeline}. * @return The {@link TrackSelectorResult}. * @throws ExoPlaybackException If an error occurs during track selection. */ public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); for (ExoTrackSelection trackSelection : selectorResult.selections) { if (trackSelection != null) { trackSelection.onPlaybackSpeed(playbackSpeed); } } return selectorResult; } /** * Applies a {@link TrackSelectorResult} to the period. * * @param trackSelectorResult The {@link TrackSelectorResult} to apply. * @param positionUs The position relative to the start of the period at which to apply the new * track selections, in microseconds. * @param forceRecreateStreams Whether all streams are forced to be recreated. * @return The actual position relative to the start of the period at which the new track * selections are applied. */ public long applyTrackSelection( TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams) { return applyTrackSelection( trackSelectorResult, positionUs, forceRecreateStreams, new boolean[rendererCapabilities.length]); } /** * Applies a {@link TrackSelectorResult} to the period. * * @param newTrackSelectorResult The {@link TrackSelectorResult} to apply. * @param positionUs The position relative to the start of the period at which to apply the new * track selections, in microseconds. * @param forceRecreateStreams Whether all streams are forced to be recreated. * @param streamResetFlags Will be populated to indicate which streams have been reset or were * newly created. * @return The actual position relative to the start of the period at which the new track * selections are applied. */ public long applyTrackSelection( TrackSelectorResult newTrackSelectorResult, long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) { for (int i = 0; i < newTrackSelectorResult.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i); } // Undo the effect of previous call to associate no-sample renderers with empty tracks // so the mediaPeriod receives back whatever it sent us before. disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); disableTrackSelectionsInResult(); trackSelectorResult = newTrackSelectorResult; enableTrackSelectionsInResult(); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks( newTrackSelectorResult.selections, mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); associateNoSampleRenderersWithEmptySampleStream(sampleStreams); // Update whether we have enabled tracks and check that the expected streams are non-null. hasEnabledTracks = false; for (int i = 0; i < sampleStreams.length; i++) { if (sampleStreams[i] != null) { Assertions.checkState(newTrackSelectorResult.isRendererEnabled(i)); // hasEnabledTracks should be true only when non-empty streams exists. if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { hasEnabledTracks = true; } } else { Assertions.checkState(newTrackSelectorResult.selections[i] == null); } } return positionUs; } /** Releases the media period. No other method should be called after the release. */ public void release() { disableTrackSelectionsInResult(); releaseMediaPeriod(mediaSourceList, mediaPeriod); } /** * Sets the next media period holder in the queue. * * @param nextMediaPeriodHolder The next holder, or null if this will be the new loading media * period holder at the end of the queue. */ public void setNext(@Nullable MediaPeriodHolder nextMediaPeriodHolder) { if (nextMediaPeriodHolder == next) { return; } disableTrackSelectionsInResult(); next = nextMediaPeriodHolder; enableTrackSelectionsInResult(); } /** * Returns the next media period holder in the queue, or null if this is the last media period * (and thus the loading media period). */ @Nullable public MediaPeriodHolder getNext() { return next; } /** Returns the {@link TrackGroupArray} exposed by this media period. */ public TrackGroupArray getTrackGroups() { return trackGroups; } /** Returns the {@link TrackSelectorResult} which is currently applied. */ public TrackSelectorResult getTrackSelectorResult() { return trackSelectorResult; } /** Updates the clipping to {@link MediaPeriodInfo#endPositionUs} if required. */ public void updateClipping() { if (mediaPeriod instanceof ClippingMediaPeriod) { long endPositionUs = info.endPositionUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE : info.endPositionUs; ((ClippingMediaPeriod) mediaPeriod).updateClipping(/* startUs= */ 0, endPositionUs); } } private void enableTrackSelectionsInResult() { if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); ExoTrackSelection trackSelection = trackSelectorResult.selections[i]; if (rendererEnabled && trackSelection != null) { trackSelection.enable(); } } } private void disableTrackSelectionsInResult() { if (!isLoadingMediaPeriod()) { return; } for (int i = 0; i < trackSelectorResult.length; i++) { boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); ExoTrackSelection trackSelection = trackSelectorResult.selections[i]; if (rendererEnabled && trackSelection != null) { trackSelection.disable(); } } } /** * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the {@link * EmptySampleStream} that was associated with it. */ private void disassociateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { sampleStreams[i] = null; } } } /** * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with * an {@link EmptySampleStream}. */ private void associateNoSampleRenderersWithEmptySampleStream( @NullableType SampleStream[] sampleStreams) { for (int i = 0; i < rendererCapabilities.length; i++) { if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE && trackSelectorResult.isRendererEnabled(i)) { sampleStreams[i] = new EmptySampleStream(); } } } private boolean isLoadingMediaPeriod() { return next == null; } /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( MediaPeriodId id, MediaSourceList mediaSourceList, Allocator allocator, long startPositionUs, long endPositionUs) { MediaPeriod mediaPeriod = mediaSourceList.createPeriod(id, allocator, startPositionUs); if (endPositionUs != C.TIME_UNSET) { mediaPeriod = new ClippingMediaPeriod( mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs); } return mediaPeriod; } /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod(MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) { try { if (mediaPeriod instanceof ClippingMediaPeriod) { mediaSourceList.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { mediaSourceList.releasePeriod(mediaPeriod); } } catch (RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Period release failed.", e); } } }