| /* |
| * 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 com.google.android.exoplayer2; |
| |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.source.ClippingMediaPeriod; |
| import com.google.android.exoplayer2.source.EmptySampleStream; |
| import com.google.android.exoplayer2.source.MediaPeriod; |
| import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; |
| import com.google.android.exoplayer2.source.SampleStream; |
| import com.google.android.exoplayer2.source.TrackGroupArray; |
| import com.google.android.exoplayer2.trackselection.TrackSelection; |
| import com.google.android.exoplayer2.trackselection.TrackSelectionArray; |
| import com.google.android.exoplayer2.trackselection.TrackSelector; |
| import com.google.android.exoplayer2.trackselection.TrackSelectorResult; |
| import com.google.android.exoplayer2.upstream.Allocator; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.Log; |
| import org.checkerframework.checker.nullness.compatqual.NullableType; |
| |
| /** 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 required renderers have been enabled with the {@link #sampleStreams} for this |
| * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[], |
| * SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[], |
| * SampleStream, long)} has been called. |
| */ |
| public boolean allRenderersEnabled; |
| |
| 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 playback speed. |
| * @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 = Math.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}. |
| * |
| * <p>The new track selection needs to be applied with {@link |
| * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. |
| * |
| * @param playbackSpeed The current playback speed. |
| * @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 (TrackSelection trackSelection : selectorResult.selections.getAll()) { |
| 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. |
| TrackSelectionArray trackSelections = newTrackSelectorResult.selections; |
| positionUs = |
| mediaPeriod.selectTracks( |
| trackSelections.getAll(), |
| mayRetainStreamFlags, |
| sampleStreams, |
| streamResetFlags, |
| positionUs); |
| associateNoSampleRenderersWithEmptySampleStream(sampleStreams); |
| |
| // Update whether we have enabled tracks and sanity check 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(trackSelections.get(i) == null); |
| } |
| } |
| return positionUs; |
| } |
| |
| /** Releases the media period. No other method should be called after the release. */ |
| public void release() { |
| disableTrackSelectionsInResult(); |
| releaseMediaPeriod(info.endPositionUs, 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; |
| } |
| |
| private void enableTrackSelectionsInResult() { |
| if (!isLoadingMediaPeriod()) { |
| return; |
| } |
| for (int i = 0; i < trackSelectorResult.length; i++) { |
| boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); |
| TrackSelection trackSelection = trackSelectorResult.selections.get(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); |
| TrackSelection trackSelection = trackSelectorResult.selections.get(i); |
| if (rendererEnabled && trackSelection != null) { |
| trackSelection.disable(); |
| } |
| } |
| } |
| |
| /** |
| * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@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 |
| * a dummy {@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 && endPositionUs != C.TIME_END_OF_SOURCE) { |
| 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( |
| long endPositionUs, MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) { |
| try { |
| if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { |
| 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); |
| } |
| } |
| } |