1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2; 17 18 import androidx.annotation.Nullable; 19 import com.google.android.exoplayer2.source.ClippingMediaPeriod; 20 import com.google.android.exoplayer2.source.EmptySampleStream; 21 import com.google.android.exoplayer2.source.MediaPeriod; 22 import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 23 import com.google.android.exoplayer2.source.SampleStream; 24 import com.google.android.exoplayer2.source.TrackGroupArray; 25 import com.google.android.exoplayer2.trackselection.TrackSelection; 26 import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 27 import com.google.android.exoplayer2.trackselection.TrackSelector; 28 import com.google.android.exoplayer2.trackselection.TrackSelectorResult; 29 import com.google.android.exoplayer2.upstream.Allocator; 30 import com.google.android.exoplayer2.util.Assertions; 31 import com.google.android.exoplayer2.util.Log; 32 import org.checkerframework.checker.nullness.compatqual.NullableType; 33 34 /** Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ 35 /* package */ final class MediaPeriodHolder { 36 37 private static final String TAG = "MediaPeriodHolder"; 38 39 /** The {@link MediaPeriod} wrapped by this class. */ 40 public final MediaPeriod mediaPeriod; 41 /** The unique timeline period identifier the media period belongs to. */ 42 public final Object uid; 43 /** 44 * The sample streams for each renderer associated with this period. May contain null elements. 45 */ 46 public final @NullableType SampleStream[] sampleStreams; 47 48 /** Whether the media period has finished preparing. */ 49 public boolean prepared; 50 /** Whether any of the tracks of this media period are enabled. */ 51 public boolean hasEnabledTracks; 52 /** {@link MediaPeriodInfo} about this media period. */ 53 public MediaPeriodInfo info; 54 /** 55 * Whether all required renderers have been enabled with the {@link #sampleStreams} for this 56 * {@link #mediaPeriod}. This means either {@link Renderer#enable(RendererConfiguration, Format[], 57 * SampleStream, long, boolean, boolean, long)} or {@link Renderer#replaceStream(Format[], 58 * SampleStream, long)} has been called. 59 */ 60 public boolean allRenderersEnabled; 61 62 private final boolean[] mayRetainStreamFlags; 63 private final RendererCapabilities[] rendererCapabilities; 64 private final TrackSelector trackSelector; 65 private final MediaSourceList mediaSourceList; 66 67 @Nullable private MediaPeriodHolder next; 68 private TrackGroupArray trackGroups; 69 private TrackSelectorResult trackSelectorResult; 70 private long rendererPositionOffsetUs; 71 72 /** 73 * Creates a new holder with information required to play it as part of a timeline. 74 * 75 * @param rendererCapabilities The renderer capabilities. 76 * @param rendererPositionOffsetUs The renderer time of the start of the period, in microseconds. 77 * @param trackSelector The track selector. 78 * @param allocator The allocator. 79 * @param mediaSourceList The playlist. 80 * @param info Information used to identify this media period in its timeline period. 81 * @param emptyTrackSelectorResult A {@link TrackSelectorResult} with empty selections for each 82 * renderer. 83 */ MediaPeriodHolder( RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, Allocator allocator, MediaSourceList mediaSourceList, MediaPeriodInfo info, TrackSelectorResult emptyTrackSelectorResult)84 public MediaPeriodHolder( 85 RendererCapabilities[] rendererCapabilities, 86 long rendererPositionOffsetUs, 87 TrackSelector trackSelector, 88 Allocator allocator, 89 MediaSourceList mediaSourceList, 90 MediaPeriodInfo info, 91 TrackSelectorResult emptyTrackSelectorResult) { 92 this.rendererCapabilities = rendererCapabilities; 93 this.rendererPositionOffsetUs = rendererPositionOffsetUs; 94 this.trackSelector = trackSelector; 95 this.mediaSourceList = mediaSourceList; 96 this.uid = info.id.periodUid; 97 this.info = info; 98 this.trackGroups = TrackGroupArray.EMPTY; 99 this.trackSelectorResult = emptyTrackSelectorResult; 100 sampleStreams = new SampleStream[rendererCapabilities.length]; 101 mayRetainStreamFlags = new boolean[rendererCapabilities.length]; 102 mediaPeriod = 103 createMediaPeriod( 104 info.id, mediaSourceList, allocator, info.startPositionUs, info.endPositionUs); 105 } 106 107 /** 108 * Converts time relative to the start of the period to the respective renderer time using {@link 109 * #getRendererOffset()}, in microseconds. 110 */ toRendererTime(long periodTimeUs)111 public long toRendererTime(long periodTimeUs) { 112 return periodTimeUs + getRendererOffset(); 113 } 114 115 /** 116 * Converts renderer time to the respective time relative to the start of the period using {@link 117 * #getRendererOffset()}, in microseconds. 118 */ toPeriodTime(long rendererTimeUs)119 public long toPeriodTime(long rendererTimeUs) { 120 return rendererTimeUs - getRendererOffset(); 121 } 122 123 /** Returns the renderer time of the start of the period, in microseconds. */ getRendererOffset()124 public long getRendererOffset() { 125 return rendererPositionOffsetUs; 126 } 127 128 /** 129 * Sets the renderer time of the start of the period, in microseconds. 130 * 131 * @param rendererPositionOffsetUs The new renderer position offset, in microseconds. 132 */ setRendererOffset(long rendererPositionOffsetUs)133 public void setRendererOffset(long rendererPositionOffsetUs) { 134 this.rendererPositionOffsetUs = rendererPositionOffsetUs; 135 } 136 137 /** Returns start position of period in renderer time. */ getStartPositionRendererTime()138 public long getStartPositionRendererTime() { 139 return info.startPositionUs + rendererPositionOffsetUs; 140 } 141 142 /** Returns whether the period is fully buffered. */ isFullyBuffered()143 public boolean isFullyBuffered() { 144 return prepared 145 && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); 146 } 147 148 /** 149 * Returns the buffered position in microseconds. If the period is buffered to the end, then the 150 * period duration is returned. 151 * 152 * @return The buffered position in microseconds. 153 */ getBufferedPositionUs()154 public long getBufferedPositionUs() { 155 if (!prepared) { 156 return info.startPositionUs; 157 } 158 long bufferedPositionUs = 159 hasEnabledTracks ? mediaPeriod.getBufferedPositionUs() : C.TIME_END_OF_SOURCE; 160 return bufferedPositionUs == C.TIME_END_OF_SOURCE ? info.durationUs : bufferedPositionUs; 161 } 162 163 /** 164 * Returns the next load time relative to the start of the period, or {@link C#TIME_END_OF_SOURCE} 165 * if loading has finished. 166 */ getNextLoadPositionUs()167 public long getNextLoadPositionUs() { 168 return !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); 169 } 170 171 /** 172 * Handles period preparation. 173 * 174 * @param playbackSpeed The current playback speed. 175 * @param timeline The current {@link Timeline}. 176 * @throws ExoPlaybackException If an error occurs during track selection. 177 */ handlePrepared(float playbackSpeed, Timeline timeline)178 public void handlePrepared(float playbackSpeed, Timeline timeline) throws ExoPlaybackException { 179 prepared = true; 180 trackGroups = mediaPeriod.getTrackGroups(); 181 TrackSelectorResult selectorResult = selectTracks(playbackSpeed, timeline); 182 long requestedStartPositionUs = info.startPositionUs; 183 if (info.durationUs != C.TIME_UNSET && requestedStartPositionUs >= info.durationUs) { 184 // Make sure start position doesn't exceed period duration. 185 requestedStartPositionUs = Math.max(0, info.durationUs - 1); 186 } 187 long newStartPositionUs = 188 applyTrackSelection( 189 selectorResult, requestedStartPositionUs, /* forceRecreateStreams= */ false); 190 rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; 191 info = info.copyWithStartPositionUs(newStartPositionUs); 192 } 193 194 /** 195 * Reevaluates the buffer of the media period at the given renderer position. Should only be 196 * called if this is the loading media period. 197 * 198 * @param rendererPositionUs The playing position in renderer time, in microseconds. 199 */ reevaluateBuffer(long rendererPositionUs)200 public void reevaluateBuffer(long rendererPositionUs) { 201 Assertions.checkState(isLoadingMediaPeriod()); 202 if (prepared) { 203 mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs)); 204 } 205 } 206 207 /** 208 * Continues loading the media period at the given renderer position. Should only be called if 209 * this is the loading media period. 210 * 211 * @param rendererPositionUs The load position in renderer time, in microseconds. 212 */ continueLoading(long rendererPositionUs)213 public void continueLoading(long rendererPositionUs) { 214 Assertions.checkState(isLoadingMediaPeriod()); 215 long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); 216 mediaPeriod.continueLoading(loadingPeriodPositionUs); 217 } 218 219 /** 220 * Selects tracks for the period. Must only be called if {@link #prepared} is {@code true}. 221 * 222 * <p>The new track selection needs to be applied with {@link 223 * #applyTrackSelection(TrackSelectorResult, long, boolean)} before taking effect. 224 * 225 * @param playbackSpeed The current playback speed. 226 * @param timeline The current {@link Timeline}. 227 * @return The {@link TrackSelectorResult}. 228 * @throws ExoPlaybackException If an error occurs during track selection. 229 */ selectTracks(float playbackSpeed, Timeline timeline)230 public TrackSelectorResult selectTracks(float playbackSpeed, Timeline timeline) 231 throws ExoPlaybackException { 232 TrackSelectorResult selectorResult = 233 trackSelector.selectTracks(rendererCapabilities, getTrackGroups(), info.id, timeline); 234 for (TrackSelection trackSelection : selectorResult.selections.getAll()) { 235 if (trackSelection != null) { 236 trackSelection.onPlaybackSpeed(playbackSpeed); 237 } 238 } 239 return selectorResult; 240 } 241 242 /** 243 * Applies a {@link TrackSelectorResult} to the period. 244 * 245 * @param trackSelectorResult The {@link TrackSelectorResult} to apply. 246 * @param positionUs The position relative to the start of the period at which to apply the new 247 * track selections, in microseconds. 248 * @param forceRecreateStreams Whether all streams are forced to be recreated. 249 * @return The actual position relative to the start of the period at which the new track 250 * selections are applied. 251 */ applyTrackSelection( TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams)252 public long applyTrackSelection( 253 TrackSelectorResult trackSelectorResult, long positionUs, boolean forceRecreateStreams) { 254 return applyTrackSelection( 255 trackSelectorResult, 256 positionUs, 257 forceRecreateStreams, 258 new boolean[rendererCapabilities.length]); 259 } 260 261 /** 262 * Applies a {@link TrackSelectorResult} to the period. 263 * 264 * @param newTrackSelectorResult The {@link TrackSelectorResult} to apply. 265 * @param positionUs The position relative to the start of the period at which to apply the new 266 * track selections, in microseconds. 267 * @param forceRecreateStreams Whether all streams are forced to be recreated. 268 * @param streamResetFlags Will be populated to indicate which streams have been reset or were 269 * newly created. 270 * @return The actual position relative to the start of the period at which the new track 271 * selections are applied. 272 */ applyTrackSelection( TrackSelectorResult newTrackSelectorResult, long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags)273 public long applyTrackSelection( 274 TrackSelectorResult newTrackSelectorResult, 275 long positionUs, 276 boolean forceRecreateStreams, 277 boolean[] streamResetFlags) { 278 for (int i = 0; i < newTrackSelectorResult.length; i++) { 279 mayRetainStreamFlags[i] = 280 !forceRecreateStreams && newTrackSelectorResult.isEquivalent(trackSelectorResult, i); 281 } 282 283 // Undo the effect of previous call to associate no-sample renderers with empty tracks 284 // so the mediaPeriod receives back whatever it sent us before. 285 disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); 286 disableTrackSelectionsInResult(); 287 trackSelectorResult = newTrackSelectorResult; 288 enableTrackSelectionsInResult(); 289 // Disable streams on the period and get new streams for updated/newly-enabled tracks. 290 TrackSelectionArray trackSelections = newTrackSelectorResult.selections; 291 positionUs = 292 mediaPeriod.selectTracks( 293 trackSelections.getAll(), 294 mayRetainStreamFlags, 295 sampleStreams, 296 streamResetFlags, 297 positionUs); 298 associateNoSampleRenderersWithEmptySampleStream(sampleStreams); 299 300 // Update whether we have enabled tracks and sanity check the expected streams are non-null. 301 hasEnabledTracks = false; 302 for (int i = 0; i < sampleStreams.length; i++) { 303 if (sampleStreams[i] != null) { 304 Assertions.checkState(newTrackSelectorResult.isRendererEnabled(i)); 305 // hasEnabledTracks should be true only when non-empty streams exists. 306 if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { 307 hasEnabledTracks = true; 308 } 309 } else { 310 Assertions.checkState(trackSelections.get(i) == null); 311 } 312 } 313 return positionUs; 314 } 315 316 /** Releases the media period. No other method should be called after the release. */ release()317 public void release() { 318 disableTrackSelectionsInResult(); 319 releaseMediaPeriod(info.endPositionUs, mediaSourceList, mediaPeriod); 320 } 321 322 /** 323 * Sets the next media period holder in the queue. 324 * 325 * @param nextMediaPeriodHolder The next holder, or null if this will be the new loading media 326 * period holder at the end of the queue. 327 */ setNext(@ullable MediaPeriodHolder nextMediaPeriodHolder)328 public void setNext(@Nullable MediaPeriodHolder nextMediaPeriodHolder) { 329 if (nextMediaPeriodHolder == next) { 330 return; 331 } 332 disableTrackSelectionsInResult(); 333 next = nextMediaPeriodHolder; 334 enableTrackSelectionsInResult(); 335 } 336 337 /** 338 * Returns the next media period holder in the queue, or null if this is the last media period 339 * (and thus the loading media period). 340 */ 341 @Nullable getNext()342 public MediaPeriodHolder getNext() { 343 return next; 344 } 345 346 /** Returns the {@link TrackGroupArray} exposed by this media period. */ getTrackGroups()347 public TrackGroupArray getTrackGroups() { 348 return trackGroups; 349 } 350 351 /** Returns the {@link TrackSelectorResult} which is currently applied. */ getTrackSelectorResult()352 public TrackSelectorResult getTrackSelectorResult() { 353 return trackSelectorResult; 354 } 355 enableTrackSelectionsInResult()356 private void enableTrackSelectionsInResult() { 357 if (!isLoadingMediaPeriod()) { 358 return; 359 } 360 for (int i = 0; i < trackSelectorResult.length; i++) { 361 boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); 362 TrackSelection trackSelection = trackSelectorResult.selections.get(i); 363 if (rendererEnabled && trackSelection != null) { 364 trackSelection.enable(); 365 } 366 } 367 } 368 disableTrackSelectionsInResult()369 private void disableTrackSelectionsInResult() { 370 if (!isLoadingMediaPeriod()) { 371 return; 372 } 373 for (int i = 0; i < trackSelectorResult.length; i++) { 374 boolean rendererEnabled = trackSelectorResult.isRendererEnabled(i); 375 TrackSelection trackSelection = trackSelectorResult.selections.get(i); 376 if (rendererEnabled && trackSelection != null) { 377 trackSelection.disable(); 378 } 379 } 380 } 381 382 /** 383 * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy {@link 384 * EmptySampleStream} that was associated with it. 385 */ disassociateNoSampleRenderersWithEmptySampleStream( @ullableType SampleStream[] sampleStreams)386 private void disassociateNoSampleRenderersWithEmptySampleStream( 387 @NullableType SampleStream[] sampleStreams) { 388 for (int i = 0; i < rendererCapabilities.length; i++) { 389 if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { 390 sampleStreams[i] = null; 391 } 392 } 393 } 394 395 /** 396 * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will associate it with 397 * a dummy {@link EmptySampleStream}. 398 */ associateNoSampleRenderersWithEmptySampleStream( @ullableType SampleStream[] sampleStreams)399 private void associateNoSampleRenderersWithEmptySampleStream( 400 @NullableType SampleStream[] sampleStreams) { 401 for (int i = 0; i < rendererCapabilities.length; i++) { 402 if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE 403 && trackSelectorResult.isRendererEnabled(i)) { 404 sampleStreams[i] = new EmptySampleStream(); 405 } 406 } 407 } 408 isLoadingMediaPeriod()409 private boolean isLoadingMediaPeriod() { 410 return next == null; 411 } 412 413 /** Returns a media period corresponding to the given {@code id}. */ createMediaPeriod( MediaPeriodId id, MediaSourceList mediaSourceList, Allocator allocator, long startPositionUs, long endPositionUs)414 private static MediaPeriod createMediaPeriod( 415 MediaPeriodId id, 416 MediaSourceList mediaSourceList, 417 Allocator allocator, 418 long startPositionUs, 419 long endPositionUs) { 420 MediaPeriod mediaPeriod = mediaSourceList.createPeriod(id, allocator, startPositionUs); 421 if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { 422 mediaPeriod = 423 new ClippingMediaPeriod( 424 mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs); 425 } 426 return mediaPeriod; 427 } 428 429 /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ releaseMediaPeriod( long endPositionUs, MediaSourceList mediaSourceList, MediaPeriod mediaPeriod)430 private static void releaseMediaPeriod( 431 long endPositionUs, MediaSourceList mediaSourceList, MediaPeriod mediaPeriod) { 432 try { 433 if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { 434 mediaSourceList.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); 435 } else { 436 mediaSourceList.releasePeriod(mediaPeriod); 437 } 438 } catch (RuntimeException e) { 439 // There's nothing we can do. 440 Log.e(TAG, "Period release failed.", e); 441 } 442 } 443 } 444