diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java new file mode 100644 index 0000000000..66cb9a1fce --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -0,0 +1,432 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import androidx.annotation.Nullable; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.ClippingMediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.EmptySampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaPeriod; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.SampleStream; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelection; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.Allocator; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.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; + + private final boolean[] mayRetainStreamFlags; + private final RendererCapabilities[] rendererCapabilities; + private final TrackSelector trackSelector; + private final MediaSource mediaSource; + + @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 mediaSource The media source that produced the media period. + * @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, + MediaSource mediaSource, + MediaPeriodInfo info, + TrackSelectorResult emptyTrackSelectorResult) { + this.rendererCapabilities = rendererCapabilities; + this.rendererPositionOffsetUs = rendererPositionOffsetUs; + this.trackSelector = trackSelector; + this.mediaSource = mediaSource; + 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, mediaSource, 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 newStartPositionUs = + applyTrackSelection( + selectorResult, info.startPositionUs, /* 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, mediaSource, 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, + MediaSource mediaSource, + Allocator allocator, + long startPositionUs, + long endPositionUs) { + MediaPeriod mediaPeriod = mediaSource.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, MediaSource mediaSource, MediaPeriod mediaPeriod) { + try { + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { + mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); + } else { + mediaSource.releasePeriod(mediaPeriod); + } + } catch (RuntimeException e) { + // There's nothing we can do. + Log.e(TAG, "Period release failed.", e); + } + } +} |