/* * Copyright (C) 2016 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 android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; 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.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.PriorityHandlerThread; import com.google.android.exoplayer2.util.StandaloneMediaClock; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** * Implements the internal behavior of {@link ExoPlayerImpl}. */ /* package */ final class ExoPlayerImplInternal implements Handler.Callback, MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener { /** * Playback position information which is read on the application's thread by * {@link ExoPlayerImpl} and read/written internally on the player's thread. */ public static final class PlaybackInfo { public final int periodIndex; public final long startPositionUs; public volatile long positionUs; public volatile long bufferedPositionUs; public PlaybackInfo(int periodIndex, long startPositionUs) { this.periodIndex = periodIndex; this.startPositionUs = startPositionUs; positionUs = startPositionUs; bufferedPositionUs = startPositionUs; } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { PlaybackInfo playbackInfo = new PlaybackInfo(periodIndex, startPositionUs); playbackInfo.positionUs = positionUs; playbackInfo.bufferedPositionUs = bufferedPositionUs; return playbackInfo; } } public static final class TrackInfo { public final TrackGroupArray groups; public final TrackSelectionArray selections; public final Object info; public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) { this.groups = groups; this.selections = selections; this.info = info; } } public static final class SourceInfo { public final Timeline timeline; public final Object manifest; public final PlaybackInfo playbackInfo; public final int seekAcks; public SourceInfo(Timeline timeline, Object manifest, PlaybackInfo playbackInfo, int seekAcks) { this.timeline = timeline; this.manifest = manifest; this.playbackInfo = playbackInfo; this.seekAcks = seekAcks; } } private static final String TAG = "ExoPlayerImplInternal"; // External messages public static final int MSG_STATE_CHANGED = 1; public static final int MSG_LOADING_CHANGED = 2; public static final int MSG_TRACKS_CHANGED = 3; public static final int MSG_SEEK_ACK = 4; public static final int MSG_POSITION_DISCONTINUITY = 5; public static final int MSG_SOURCE_INFO_REFRESHED = 6; public static final int MSG_ERROR = 7; // Internal messages private static final int MSG_PREPARE = 0; private static final int MSG_SET_PLAY_WHEN_READY = 1; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; private static final int MSG_STOP = 4; private static final int MSG_RELEASE = 5; private static final int MSG_REFRESH_SOURCE_INFO = 6; private static final int MSG_PERIOD_PREPARED = 7; private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 8; private static final int MSG_TRACK_SELECTION_INVALIDATED = 9; private static final int MSG_CUSTOM = 10; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; private static final int IDLE_INTERVAL_MS = 1000; /** * Limits the maximum number of periods to buffer ahead of the current playing period. The * buffering policy normally prevents buffering too far ahead, but the policy could allow too many * small periods to be buffered if the period count were not limited. */ private static final int MAXIMUM_BUFFER_AHEAD_PERIODS = 100; /** * Offset added to all sample timestamps read by renderers to make them non-negative. This is * provided for convenience of sources that may return negative timestamps due to prerolling * samples from a keyframe before their first sample with timestamp zero, so it must be set to a * value greater than or equal to the maximum key-frame interval in seekable periods. */ private static final int RENDERER_TIMESTAMP_OFFSET_US = 60000000; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; private final LoadControl loadControl; private final StandaloneMediaClock standaloneMediaClock; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final ExoPlayer player; private final Timeline.Window window; private final Timeline.Period period; private PlaybackInfo playbackInfo; private Renderer rendererMediaClockSource; private MediaClock rendererMediaClock; private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; private boolean playWhenReady; private boolean rebuffering; private boolean isLoading; private int state; private int customMessagesSent; private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingInitialSeekCount; private SeekPosition pendingSeekPosition; private long rendererPositionUs; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder playingPeriodHolder; private Timeline timeline; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, boolean playWhenReady, Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.eventHandler = eventHandler; this.state = ExoPlayer.STATE_IDLE; this.playbackInfo = playbackInfo; this.player = player; rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } standaloneMediaClock = new StandaloneMediaClock(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); trackSelector.init(this); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can // not normally change to this priority" is incorrect. internalPlaybackThread = new PriorityHandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); handler = new Handler(internalPlaybackThread.getLooper(), this); } public void prepare(MediaSource mediaSource, boolean resetPosition) { handler.obtainMessage(MSG_PREPARE, resetPosition ? 1 : 0, 0, mediaSource) .sendToTarget(); } public void setPlayWhenReady(boolean playWhenReady) { handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } public void seekTo(Timeline timeline, int windowIndex, long positionUs) { handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); } public void stop() { handler.sendEmptyMessage(MSG_STOP); } public void sendMessages(ExoPlayerMessage... messages) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); return; } customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); } public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); return; } int messageNumber = customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); while (customMessagesProcessed <= messageNumber) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public synchronized void release() { if (released) { return; } handler.sendEmptyMessage(MSG_RELEASE); while (!released) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } internalPlaybackThread.quit(); } // MediaSource.Listener implementation. @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { handler.obtainMessage(MSG_REFRESH_SOURCE_INFO, Pair.create(timeline, manifest)).sendToTarget(); } // MediaPeriod.Callback implementation. @Override public void onPrepared(MediaPeriod source) { handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget(); } @Override public void onContinueLoadingRequested(MediaPeriod source) { handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget(); } // TrackSelector.InvalidationListener implementation. @Override public void onTrackSelectionsInvalidated() { handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } // Handler.Callback implementation. @SuppressWarnings("unchecked") @Override public boolean handleMessage(Message msg) { try { switch (msg.what) { case MSG_PREPARE: { prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); return true; } case MSG_SET_PLAY_WHEN_READY: { setPlayWhenReadyInternal(msg.arg1 != 0); return true; } case MSG_DO_SOME_WORK: { doSomeWork(); return true; } case MSG_SEEK_TO: { seekToInternal((SeekPosition) msg.obj); return true; } case MSG_STOP: { stopInternal(); return true; } case MSG_RELEASE: { releaseInternal(); return true; } case MSG_PERIOD_PREPARED: { handlePeriodPrepared((MediaPeriod) msg.obj); return true; } case MSG_REFRESH_SOURCE_INFO: { handleSourceInfoRefreshed((Pair) msg.obj); return true; } case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { handleContinueLoadingRequested((MediaPeriod) msg.obj); return true; } case MSG_TRACK_SELECTION_INVALIDATED: { reselectTracksInternal(); return true; } case MSG_CUSTOM: { sendMessagesInternal((ExoPlayerMessage[]) msg.obj); return true; } default: return false; } } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); stopInternal(); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); stopInternal(); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); stopInternal(); return true; } } // Private methods. private void setState(int state) { if (this.state != state) { this.state = state; eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); } } private void setIsLoading(boolean isLoading) { if (this.isLoading != isLoading) { this.isLoading = isLoading; eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget(); } } private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { resetInternal(true); loadControl.onPrepared(); if (resetPosition) { playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); } this.mediaSource = mediaSource; mediaSource.prepareSource(player, true, this); setState(ExoPlayer.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException { rebuffering = false; this.playWhenReady = playWhenReady; if (!playWhenReady) { stopRenderers(); updatePlaybackPositions(); } else { if (state == ExoPlayer.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } else if (state == ExoPlayer.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } } private void startRenderers() throws ExoPlaybackException { rebuffering = false; standaloneMediaClock.start(); for (Renderer renderer : enabledRenderers) { renderer.start(); } } private void stopRenderers() throws ExoPlaybackException { standaloneMediaClock.stop(); for (Renderer renderer : enabledRenderers) { ensureStopped(renderer); } } private void updatePlaybackPositions() throws ExoPlaybackException { if (playingPeriodHolder == null) { return; } // Update the playback position. long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); } else { if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) { rendererPositionUs = rendererMediaClock.getPositionUs(); standaloneMediaClock.setPositionUs(rendererPositionUs); } else { rendererPositionUs = standaloneMediaClock.getPositionUs(); } periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); } playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE : playingPeriodHolder.mediaPeriod.getBufferedPositionUs(); playbackInfo.bufferedPositionUs = bufferedPositionUs == C.TIME_END_OF_SOURCE ? timeline.getPeriod(playingPeriodHolder.index, period).getDurationUs() : bufferedPositionUs; } private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = SystemClock.elapsedRealtime(); updatePeriods(); if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. maybeThrowPeriodPrepareError(); scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); return; } TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; for (Renderer renderer : enabledRenderers) { // TODO: Each renderer should return the maximum delay before which it wishes to be called // again. The minimum of these values should then be used as the delay before the next // invocation of this method. renderer.render(rendererPositionUs, elapsedRealtimeUs); allRenderersEnded = allRenderersEnded && renderer.isEnded(); // Determine whether the renderer is ready (or ended). If it's not, throw an error that's // preventing the renderer from making progress, if such an error exists. boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded(); if (!rendererReadyOrEnded) { renderer.maybeThrowStreamError(); } allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; } if (!allRenderersReadyOrEnded) { maybeThrowPeriodPrepareError(); } long playingPeriodDurationUs = timeline.getPeriod(playingPeriodHolder.index, period) .getDurationUs(); if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) && playingPeriodHolder.isLast) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { boolean isNewlyReady = enabledRenderers.length > 0 ? (allRenderersReadyOrEnded && haveSufficientBuffer(rebuffering)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { startRenderers(); } } } else if (state == ExoPlayer.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { rebuffering = playWhenReady; setState(ExoPlayer.STATE_BUFFERING); stopRenderers(); } } if (state == ExoPlayer.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); } else if (enabledRenderers.length != 0) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); } TraceUtil.endSection(); } private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs; long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime(); if (nextOperationDelayMs <= 0) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } else { handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs); } } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { if (timeline == null) { pendingInitialSeekCount++; pendingSeekPosition = seekPosition; return; } Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. playbackInfo = new PlaybackInfo(0, 0); eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't // ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); setState(ExoPlayer.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); return; } int periodIndex = periodPosition.first; long periodPositionUs = periodPosition.second; try { if (periodIndex == playbackInfo.periodIndex && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { // Seek position equals the current position. Do nothing. return; } periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs); } finally { playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget(); } } private long seekToPeriodPosition(int periodIndex, long periodPositionUs) throws ExoPlaybackException { stopRenderers(); rebuffering = false; setState(ExoPlayer.STATE_BUFFERING); MediaPeriodHolder newPlayingPeriodHolder = null; if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. if (loadingPeriodHolder != null) { loadingPeriodHolder.release(); } } else { // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { if (periodHolder.index == periodIndex && periodHolder.prepared) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); } periodHolder = periodHolder.next; } } // Disable all the renderers if the period being played is changing, or if the renderers are // reading from a period other than the one being played. if (playingPeriodHolder != newPlayingPeriodHolder || playingPeriodHolder != readingPeriodHolder) { for (Renderer renderer : enabledRenderers) { renderer.disable(); } enabledRenderers = new Renderer[0]; rendererMediaClock = null; rendererMediaClockSource = null; } // Update the holders. if (newPlayingPeriodHolder != null) { newPlayingPeriodHolder.next = null; loadingPeriodHolder = newPlayingPeriodHolder; readingPeriodHolder = newPlayingPeriodHolder; setPlayingPeriodHolder(newPlayingPeriodHolder); if (playingPeriodHolder.hasEnabledTracks) { periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { loadingPeriodHolder = null; readingPeriodHolder = null; playingPeriodHolder = null; resetRendererPosition(periodPositionUs); } handler.sendEmptyMessage(MSG_DO_SOME_WORK); return periodPositionUs; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US : playingPeriodHolder.toRendererTime(periodPositionUs); standaloneMediaClock.setPositionUs(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); } } private void stopInternal() { resetInternal(true); loadControl.onStopped(); setState(ExoPlayer.STATE_IDLE); } private void releaseInternal() { resetInternal(true); loadControl.onReleased(); setState(ExoPlayer.STATE_IDLE); synchronized (this) { released = true; notifyAll(); } } private void resetInternal(boolean releaseMediaSource) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; standaloneMediaClock.stop(); rendererMediaClock = null; rendererMediaClockSource = null; rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { ensureStopped(renderer); renderer.disable(); } catch (ExoPlaybackException | RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Stop failed.", e); } } enabledRenderers = new Renderer[0]; releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder); loadingPeriodHolder = null; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); mediaSource = null; } timeline = null; } } private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { try { for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } if (mediaSource != null) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } finally { synchronized (this) { customMessagesProcessed++; notifyAll(); } } } private void ensureStopped(Renderer renderer) throws ExoPlaybackException { if (renderer.getState() == Renderer.STATE_STARTED) { renderer.stop(); } } private void reselectTracksInternal() throws ExoPlaybackException { if (playingPeriodHolder == null) { // We don't have tracks yet, so we don't care. return; } // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = playingPeriodHolder; boolean selectionsChangedForReadPeriod = true; while (true) { if (periodHolder == null || !periodHolder.prepared) { // The reselection did not change any prepared periods. return; } if (periodHolder.selectTracks()) { // Selected tracks have changed for this period. break; } if (periodHolder == readingPeriodHolder) { // The track reselection didn't affect any period that has been read. selectionsChangedForReadPeriod = false; } periodHolder = periodHolder.next; } if (selectionsChangedForReadPeriod) { // Update streams and rebuffer for the new selection, recreating all streams if reading ahead. boolean recreateStreams = readingPeriodHolder != playingPeriodHolder; releasePeriodHoldersFrom(playingPeriodHolder.next); playingPeriodHolder.next = null; loadingPeriodHolder = playingPeriodHolder; readingPeriodHolder = playingPeriodHolder; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { playbackInfo.positionUs = periodPositionUs; resetRendererPosition(periodPositionUs); } int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; SampleStream sampleStream = playingPeriodHolder.sampleStreams[i]; if (sampleStream != null) { enabledRendererCount++; } if (rendererWasEnabledFlags[i]) { if (sampleStream != renderer.getStream()) { // We need to disable the renderer. if (renderer == rendererMediaClockSource) { // The renderer is providing the media clock. if (sampleStream == null) { // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take // over timing responsibilities. standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); } rendererMediaClock = null; rendererMediaClockSource = null; } ensureStopped(renderer); renderer.disable(); } else if (streamResetFlags[i]) { // The renderer will continue to consume from its current stream, but needs to be reset. renderer.resetPosition(rendererPositionUs); } } } eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. loadingPeriodHolder = periodHolder; periodHolder = loadingPeriodHolder.next; while (periodHolder != null) { periodHolder.release(); periodHolder = periodHolder.next; } loadingPeriodHolder.next = null; if (loadingPeriodHolder.prepared) { long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.startPositionUs, loadingPeriodHolder.toPeriodTime(rendererPositionUs)); loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } maybeContinueLoading(); updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs || (playingPeriodHolder.next != null && playingPeriodHolder.next.prepared); } private boolean haveSufficientBuffer(boolean rebuffering) { long loadingPeriodBufferedPositionUs = !loadingPeriodHolder.prepared ? loadingPeriodHolder.startPositionUs : loadingPeriodHolder.mediaPeriod.getBufferedPositionUs(); if (loadingPeriodBufferedPositionUs == C.TIME_END_OF_SOURCE) { if (loadingPeriodHolder.isLast) { return true; } loadingPeriodBufferedPositionUs = timeline.getPeriod(loadingPeriodHolder.index, period) .getDurationUs(); } return loadControl.shouldStartPlayback( loadingPeriodBufferedPositionUs - loadingPeriodHolder.toPeriodTime(rendererPositionUs), rebuffering); } private void maybeThrowPeriodPrepareError() throws IOException { if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) { for (Renderer renderer : enabledRenderers) { if (!renderer.hasReadStreamToEnd()) { return; } } loadingPeriodHolder.mediaPeriod.maybeThrowPrepareError(); } } private void handleSourceInfoRefreshed(Pair timelineAndManifest) throws ExoPlaybackException { Timeline oldTimeline = timeline; timeline = timelineAndManifest.first; Object manifest = timelineAndManifest.second; int processedInitialSeekCount = 0; if (oldTimeline == null) { if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); processedInitialSeekCount = pendingInitialSeekCount; pendingInitialSeekCount = 0; pendingSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } playbackInfo = new PlaybackInfo(periodPosition.first, periodPosition.second); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); playbackInfo = new PlaybackInfo(defaultPosition.first, defaultPosition.second); } } MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null) { // We don't have any period holders, so we're done. notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. int newPeriodIndex = resolveSubsequentPeriod(periodHolder.index, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); return; } // We resolved a subsequent period. Seek to the default position in the corresponding window. Pair defaultPosition = getPeriodPosition( timeline.getPeriod(newPeriodIndex, period).windowIndex, C.TIME_UNSET); newPeriodIndex = defaultPosition.first; long newPositionUs = defaultPosition.second; timeline.getPeriod(newPeriodIndex, period, true); // Clear the index of each holder that doesn't contain the default position. If a holder // contains the default position then update its index so it can be re-used when seeking. Object newPeriodUid = period.uid; periodHolder.index = C.INDEX_UNSET; while (periodHolder.next != null) { periodHolder = periodHolder.next; periodHolder.index = periodHolder.uid.equals(newPeriodUid) ? newPeriodIndex : C.INDEX_UNSET; } // Actually do the seek. newPositionUs = seekToPeriodPosition(newPeriodIndex, newPositionUs); playbackInfo = new PlaybackInfo(newPeriodIndex, newPositionUs); notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } // The current period is in the new timeline. Update the holder and playbackInfo. timeline.getPeriod(periodIndex, period); boolean isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 && !timeline.getWindow(period.windowIndex, window).isDynamic; periodHolder.setIndex(periodIndex, isLastPeriod); boolean seenReadingPeriod = periodHolder == readingPeriodHolder; if (periodIndex != playbackInfo.periodIndex) { playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); } // If there are subsequent holders, update the index for each of them. If we find a holder // that's inconsistent with the new timeline then take appropriate action. while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; periodIndex++; timeline.getPeriod(periodIndex, period, true); isLastPeriod = periodIndex == timeline.getPeriodCount() - 1 && !timeline.getWindow(period.windowIndex, window).isDynamic; if (periodHolder.uid.equals(period.uid)) { // The holder is consistent with the new timeline. Update its index and continue. periodHolder.setIndex(periodIndex, isLastPeriod); seenReadingPeriod |= (periodHolder == readingPeriodHolder); } else { // The holder is inconsistent with the new timeline. if (!seenReadingPeriod) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. periodIndex = playingPeriodHolder.index; long newPositionUs = seekToPeriodPosition(periodIndex, playbackInfo.positionUs); playbackInfo = new PlaybackInfo(periodIndex, newPositionUs); } else { // Update the loading period to be the last period that's still valid, and release all // subsequent periods. loadingPeriodHolder = previousPeriodHolder; loadingPeriodHolder.next = null; // Release the rest of the timeline. releasePeriodHoldersFrom(periodHolder); } break; } } notifySourceInfoRefresh(manifest, processedInitialSeekCount); } private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { // Set the playback position to (0,0) for notifying the eventHandler. playbackInfo = new PlaybackInfo(0, 0); notifySourceInfoRefresh(manifest, processedInitialSeekCount); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); setState(ExoPlayer.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); } private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); } /** * Given a period index into an old timeline, finds the first subsequent period that also exists * in a new timeline. The index of this period in the new timeline is returned. * * @param oldPeriodIndex The index of the period in the old timeline. * @param oldTimeline The old timeline. * @param newTimeline The new timeline. * @return The index in the new timeline of the first subsequent period, or {@link C#INDEX_UNSET} * if no such period was found. */ private int resolveSubsequentPeriod(int oldPeriodIndex, Timeline oldTimeline, Timeline newTimeline) { int newPeriodIndex = C.INDEX_UNSET; while (newPeriodIndex == C.INDEX_UNSET && oldPeriodIndex < oldTimeline.getPeriodCount() - 1) { newPeriodIndex = newTimeline.getIndexOfPeriod( oldTimeline.getPeriod(++oldPeriodIndex, period, true).uid); } return newPeriodIndex; } /** * Converts a {@link SeekPosition} into the corresponding (periodIndex, periodPositionUs) for the * internal timeline. * * @param seekPosition The position to resolve. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ private Pair resolveSeekPosition(SeekPosition seekPosition) { Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { // The application performed a blind seek without a non-empty timeline (most likely based on // knowledge of what the future timeline will be). Use the internal timeline. seekTimeline = timeline; } // Map the SeekPosition to a position in the corresponding timeline. Pair periodPosition; try { periodPosition = getPeriodPosition(seekTimeline, seekPosition.windowIndex, seekPosition.windowPositionUs); } catch (IndexOutOfBoundsException e) { // The window index of the seek position was outside the bounds of the timeline. throw new IllegalSeekPositionException(timeline, seekPosition.windowIndex, seekPosition.windowPositionUs); } if (timeline == seekTimeline) { // Our internal timeline is the seek timeline, so the mapped position is correct. return periodPosition; } // Attempt to find the mapped period in the internal timeline. int periodIndex = timeline.getIndexOfPeriod( seekTimeline.getPeriod(periodPosition.first, period, true).uid); if (periodIndex != C.INDEX_UNSET) { // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } // Try and find a subsequent period from the seek timeline in the internal timeline. periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); if (periodIndex != C.INDEX_UNSET) { // We found one. Map the SeekPosition onto the corresponding default position. return getPeriodPosition(timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); } // We didn't find one. Give up. return null; } /** * Calls {@link #getPeriodPosition(Timeline, int, long)} using the current timeline. */ private Pair getPeriodPosition(int windowIndex, long windowPositionUs) { return getPeriodPosition(timeline, windowIndex, windowPositionUs); } /** * Calls {@link #getPeriodPosition(Timeline, int, long, long)} with a zero default position * projection. */ private Pair getPeriodPosition(Timeline timeline, int windowIndex, long windowPositionUs) { return getPeriodPosition(timeline, windowIndex, windowPositionUs, 0); } /** * Converts (windowIndex, windowPositionUs) to the corresponding (periodIndex, periodPositionUs). * * @param timeline The timeline containing the window. * @param windowIndex The window index. * @param windowPositionUs The window time, or {@link C#TIME_UNSET} to use the window's default * start position. * @param defaultPositionProjectionUs If {@code windowPositionUs} is {@link C#TIME_UNSET}, the * duration into the future by which the window's position should be projected. * @return The corresponding (periodIndex, periodPositionUs), or null if {@code #windowPositionUs} * is {@link C#TIME_UNSET}, {@code defaultPositionProjectionUs} is non-zero, and the window's * position could not be projected by {@code defaultPositionProjectionUs}. */ private Pair getPeriodPosition(Timeline timeline, int windowIndex, long windowPositionUs, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount()); timeline.getWindow(windowIndex, window, false, defaultPositionProjectionUs); if (windowPositionUs == C.TIME_UNSET) { windowPositionUs = window.getDefaultPositionUs(); if (windowPositionUs == C.TIME_UNSET) { return null; } } int periodIndex = window.firstPeriodIndex; long periodPositionUs = window.getPositionInFirstPeriodUs() + windowPositionUs; long periodDurationUs = timeline.getPeriod(periodIndex, period).getDurationUs(); while (periodDurationUs != C.TIME_UNSET && periodPositionUs >= periodDurationUs && periodIndex < window.lastPeriodIndex) { periodPositionUs -= periodDurationUs; periodDurationUs = timeline.getPeriod(++periodIndex, period).getDurationUs(); } return Pair.create(periodIndex, periodPositionUs); } private void updatePeriods() throws ExoPlaybackException, IOException { if (timeline == null) { // We're waiting to get information about periods. mediaSource.maybeThrowSourceInfoRefreshError(); return; } // Update the loading period if required. maybeUpdateLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && loadingPeriodHolder.needsContinueLoading) { maybeContinueLoading(); } if (playingPeriodHolder == null) { // We're waiting for the first period to be prepared. return; } // Update the playing and reading periods. while (playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = new PlaybackInfo(playingPeriodHolder.index, playingPeriodHolder.startPositionUs); updatePlaybackPositions(); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); } if (readingPeriodHolder.isLast) { for (Renderer renderer : enabledRenderers) { // 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. if (renderer.hasReadStreamToEnd()) { renderer.setCurrentStreamFinal(); } } return; } for (Renderer renderer : enabledRenderers) { if (!renderer.hasReadStreamToEnd()) { return; } } if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections; readingPeriodHolder = readingPeriodHolder.next; TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections; boolean initialDiscontinuity = readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; TrackSelection oldSelection = oldTrackSelections.get(i); TrackSelection newSelection = newTrackSelections.get(i); if (oldSelection == null) { // The renderer has no current stream and will be enabled when we play the next period. } else if (initialDiscontinuity) { // The new period starts with a discontinuity, so the renderer will play out all data then // be disabled and re-enabled when it starts playing the next period. renderer.setCurrentStreamFinal(); } else if (!renderer.isCurrentStreamFinal()) { if (newSelection != null) { // Replace the renderer's SampleStream so the transition to playing the next period // can be seamless. Format[] formats = new Format[newSelection.length()]; for (int j = 0; j < formats.length; j++) { formats[j] = newSelection.getFormat(j); } renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { // The renderer will be disabled when transitioning to playing the next period. Mark the // SampleStream as final to play out any remaining data. renderer.setCurrentStreamFinal(); } } } } } private void maybeUpdateLoadingPeriod() throws IOException { int newLoadingPeriodIndex; if (loadingPeriodHolder == null) { newLoadingPeriodIndex = playbackInfo.periodIndex; } else { int loadingPeriodIndex = loadingPeriodHolder.index; if (loadingPeriodHolder.isLast || !loadingPeriodHolder.isFullyBuffered() || timeline.getPeriod(loadingPeriodIndex, period).getDurationUs() == C.TIME_UNSET) { // Either the existing loading period is the last period, or we are not ready to advance to // loading the next period because it hasn't been fully buffered or its duration is unknown. return; } if (playingPeriodHolder != null && loadingPeriodIndex - playingPeriodHolder.index == MAXIMUM_BUFFER_AHEAD_PERIODS) { // We are already buffering the maximum number of periods ahead. return; } newLoadingPeriodIndex = loadingPeriodHolder.index + 1; } if (newLoadingPeriodIndex >= timeline.getPeriodCount()) { // The next period is not available yet. mediaSource.maybeThrowSourceInfoRefreshError(); return; } long newLoadingPeriodStartPositionUs; if (loadingPeriodHolder == null) { newLoadingPeriodStartPositionUs = playbackInfo.startPositionUs; } else { int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex; if (newLoadingPeriodIndex != timeline.getWindow(newLoadingWindowIndex, window).firstPeriodIndex) { // We're starting to buffer a new period in the current window. Always start from the // beginning of the period. newLoadingPeriodStartPositionUs = 0; } else { // We're starting to buffer a new window. When playback transitions to this window we'll // want it to be from its default start position. The expected delay until playback // transitions is equal the duration of media that's currently buffered (assuming no // interruptions). Hence we project the default start position forward by the duration of // the buffer, and start buffering from this point. long defaultPositionProjectionUs = loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs() - rendererPositionUs; Pair defaultPosition = getPeriodPosition(timeline, newLoadingWindowIndex, C.TIME_UNSET, Math.max(0, defaultPositionProjectionUs)); if (defaultPosition == null) { return; } newLoadingPeriodIndex = defaultPosition.first; newLoadingPeriodStartPositionUs = defaultPosition.second; } } long rendererPositionOffsetUs = loadingPeriodHolder == null ? newLoadingPeriodStartPositionUs + RENDERER_TIMESTAMP_OFFSET_US : (loadingPeriodHolder.getRendererOffset() + timeline.getPeriod(loadingPeriodHolder.index, period).getDurationUs()); timeline.getPeriod(newLoadingPeriodIndex, period, true); boolean isLastPeriod = newLoadingPeriodIndex == timeline.getPeriodCount() - 1 && !timeline.getWindow(period.windowIndex, window).isDynamic; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, period.uid, newLoadingPeriodIndex, isLastPeriod, newLoadingPeriodStartPositionUs); if (loadingPeriodHolder != null) { loadingPeriodHolder.next = newPeriodHolder; } loadingPeriodHolder = newPeriodHolder; loadingPeriodHolder.mediaPeriod.prepare(this); setIsLoading(true); } private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; } loadingPeriodHolder.handlePrepared(); if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; resetRendererPosition(readingPeriodHolder.startPositionUs); setPlayingPeriodHolder(readingPeriodHolder); } maybeContinueLoading(); } private void handleContinueLoadingRequested(MediaPeriod period) { if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; } maybeContinueLoading(); } private void maybeContinueLoading() { long nextLoadPositionUs = !loadingPeriodHolder.prepared ? 0 : loadingPeriodHolder.mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { setIsLoading(false); } else { long loadingPeriodPositionUs = loadingPeriodHolder.toPeriodTime(rendererPositionUs); long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; boolean continueLoading = loadControl.shouldContinueLoading(bufferedDurationUs); setIsLoading(continueLoading); if (continueLoading) { loadingPeriodHolder.needsContinueLoading = false; loadingPeriodHolder.mediaPeriod.continueLoading(loadingPeriodPositionUs); } else { loadingPeriodHolder.needsContinueLoading = true; } } } private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { while (periodHolder != null) { periodHolder.release(); periodHolder = periodHolder.next; } } private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { if (playingPeriodHolder == periodHolder) { return; } playingPeriodHolder = periodHolder; int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; TrackSelection newSelection = periodHolder.trackSelections.get(i); if (newSelection != null) { enabledRendererCount++; } if (rendererWasEnabledFlags[i] && (newSelection == null || renderer.isCurrentStreamFinal())) { // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to disable and re-enable it because // the renderer thinks that its current stream is final. if (renderer == rendererMediaClockSource) { // Sync standaloneMediaClock so that it can take over timing responsibilities. standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); rendererMediaClock = null; rendererMediaClockSource = null; } ensureStopped(renderer); renderer.disable(); } } eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) throws ExoPlaybackException { enabledRenderers = new Renderer[enabledRendererCount]; enabledRendererCount = 0; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i); if (newSelection != null) { enabledRenderers[enabledRendererCount++] = renderer; if (renderer.getState() == Renderer.STATE_DISABLED) { // The renderer needs enabling with its new track selection. boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !rendererWasEnabledFlags[i] && playing; // Build an array of formats contained by the selection. Format[] formats = new Format[newSelection.length()]; for (int j = 0; j < formats.length; j++) { formats[j] = newSelection.getFormat(j); } // Enable the renderer. renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); MediaClock mediaClock = renderer.getMediaClock(); if (mediaClock != null) { if (rendererMediaClock != null) { throw ExoPlaybackException.createForUnexpected( new IllegalStateException("Multiple renderer media clocks enabled.")); } rendererMediaClock = mediaClock; rendererMediaClockSource = renderer; } // Start the renderer if playing. if (playing) { renderer.start(); } } } } } /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ private static final class MediaPeriodHolder { public final MediaPeriod mediaPeriod; public final Object uid; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; public final long rendererPositionOffsetUs; public int index; public long startPositionUs; public boolean isLast; public boolean prepared; public boolean hasEnabledTracks; public MediaPeriodHolder next; public boolean needsContinueLoading; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; private final LoadControl loadControl; private final MediaSource mediaSource; private Object trackSelectionsInfo; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; private TrackSelectionArray periodTrackSelections; public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, MediaSource mediaSource, Object periodUid, int periodIndex, boolean isLastPeriod, long startPositionUs) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs; this.trackSelector = trackSelector; this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); this.index = periodIndex; this.isLast = isLastPeriod; this.startPositionUs = startPositionUs; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; mediaPeriod = mediaSource.createPeriod(periodIndex, loadControl.getAllocator(), startPositionUs); } public long toRendererTime(long periodTimeUs) { return periodTimeUs + getRendererOffset(); } public long toPeriodTime(long rendererTimeUs) { return rendererTimeUs - getRendererOffset(); } public long getRendererOffset() { return rendererPositionOffsetUs - startPositionUs; } public void setIndex(int index, boolean isLast) { this.index = index; this.isLast = isLast; } public boolean isFullyBuffered() { return prepared && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } public void handlePrepared() throws ExoPlaybackException { prepared = true; trackGroups = mediaPeriod.getTrackGroups(); selectTracks(); startPositionUs = updatePeriodTrackSelection(startPositionUs, false); } public boolean selectTracks() throws ExoPlaybackException { Pair selectorResult = trackSelector.selectTracks( rendererCapabilities, trackGroups); TrackSelectionArray newTrackSelections = selectorResult.first; if (newTrackSelections.equals(periodTrackSelections)) { return false; } trackSelections = newTrackSelections; trackSelectionsInfo = selectorResult.second; return true; } public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams) { return updatePeriodTrackSelection(positionUs, forceRecreateStreams, new boolean[renderers.length]); } public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams, boolean[] streamResetFlags) { for (int i = 0; i < trackSelections.length; i++) { mayRetainStreamFlags[i] = !forceRecreateStreams && Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), trackSelections.get(i)); } // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); periodTrackSelections = trackSelections; // 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(trackSelections.get(i) != null); hasEnabledTracks = true; } else { Assertions.checkState(trackSelections.get(i) == null); } } // The track selection has changed. loadControl.onTracksSelected(renderers, trackGroups, trackSelections); return positionUs; } public TrackInfo getTrackInfo() { return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo); } public void release() { try { mediaSource.releasePeriod(mediaPeriod); } catch (RuntimeException e) { // There's nothing we can do. Log.e(TAG, "Period release failed.", e); } } } private static final class SeekPosition { public final Timeline timeline; public final int windowIndex; public final long windowPositionUs; public SeekPosition(Timeline timeline, int windowIndex, long windowPositionUs) { this.timeline = timeline; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } } }