/*
* Copyright (C) 2014 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.exoplayer;
import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.PriorityHandlerThread;
import com.google.android.exoplayer.util.TraceUtil;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.util.Log;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Implements the internal behavior of {@link ExoPlayerImpl}.
*/
/* package */ final class ExoPlayerImplInternal implements Handler.Callback {
private static final String TAG = "ExoPlayerImplInternal";
// External messages
public static final int MSG_STATE_CHANGED = 1;
public static final int MSG_SET_PLAY_WHEN_READY_ACK = 2;
public static final int MSG_ERROR = 3;
// Internal messages
private static final int MSG_PREPARE = 1;
private static final int MSG_INCREMENTAL_PREPARE = 2;
private static final int MSG_SET_PLAY_WHEN_READY = 3;
private static final int MSG_STOP = 4;
private static final int MSG_RELEASE = 5;
private static final int MSG_SEEK_TO = 6;
private static final int MSG_DO_SOME_WORK = 7;
private static final int MSG_SET_RENDERER_ENABLED = 8;
private static final int MSG_CUSTOM = 9;
private static final int PREPARE_INTERVAL_MS = 10;
private static final int RENDERING_INTERVAL_MS = 10;
private static final int IDLE_INTERVAL_MS = 1000;
private final Handler handler;
private final HandlerThread internalPlaybackThread;
private final Handler eventHandler;
private final MediaClock mediaClock;
private final boolean[] rendererEnabledFlags;
private final long minBufferUs;
private final long minRebufferUs;
private final List<TrackRenderer> enabledRenderers;
private TrackRenderer[] renderers;
private TrackRenderer timeSourceTrackRenderer;
private boolean released;
private boolean playWhenReady;
private boolean rebuffering;
private int state;
private int customMessagesSent = 0;
private int customMessagesProcessed = 0;
private long elapsedRealtimeUs;
private volatile long durationUs;
private volatile long positionUs;
private volatile long bufferedPositionUs;
public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) {
this.eventHandler = eventHandler;
this.playWhenReady = playWhenReady;
this.rendererEnabledFlags = new boolean[rendererEnabledFlags.length];
this.minBufferUs = minBufferMs * 1000L;
this.minRebufferUs = minRebufferMs * 1000L;
for (int i = 0; i < rendererEnabledFlags.length; i++) {
this.rendererEnabledFlags[i] = rendererEnabledFlags[i];
}
this.state = ExoPlayer.STATE_IDLE;
this.durationUs = TrackRenderer.UNKNOWN_TIME_US;
this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
mediaClock = new MediaClock();
enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",
Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
handler = new Handler(internalPlaybackThread.getLooper(), this);
}
public Looper getPlaybackLooper() {
return internalPlaybackThread.getLooper();
}
public long getCurrentPosition() {
return positionUs / 1000;
}
public long getBufferedPosition() {
return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: bufferedPositionUs / 1000;
}
public long getDuration() {
return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
: durationUs / 1000;
}
public void prepare(TrackRenderer... renderers) {
handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget();
}
public void setPlayWhenReady(boolean playWhenReady) {
handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
}
public void seekTo(long positionMs) {
handler.obtainMessage(MSG_SEEK_TO, positionMs).sendToTarget();
}
public void stop() {
handler.sendEmptyMessage(MSG_STOP);
}
public void setRendererEnabled(int index, boolean enabled) {
System.out.println(">>>ExoPlayerImplInternal.setRendererEnabled("+index+","+enabled+")");
if(!enabled)
Thread.dumpStack();
Log.d("ExoPlayerImplInternal", "#####setRendererEnabled(): "+Arrays.toString(Thread.currentThread().getStackTrace()));
handler.obtainMessage(MSG_SET_RENDERER_ENABLED, index, enabled ? 1 : 0).sendToTarget();
}
public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
}
public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
Object message) {
if (released) {
Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored.");
return;
}
int messageNumber = customMessagesSent++;
handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
while (customMessagesProcessed <= messageNumber) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public synchronized void release() {
System.out.println("ExoPlayerImplInternal.release() released = "+released);
if (released) {
return;
}
handler.sendEmptyMessage(MSG_RELEASE);
while (!released) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
internalPlaybackThread.quit();
}
@Override
public boolean handleMessage(Message msg) {
try {
if(msg.what != MSG_DO_SOME_WORK)
System.out.println(">>>> ExoPlayerImplInternal.handleMessage() msg = "+msg.what);
switch (msg.what) {
case MSG_PREPARE: {
prepareInternal((TrackRenderer[]) msg.obj);
return true;
}
case MSG_INCREMENTAL_PREPARE: {
incrementalPrepareInternal();
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((Long) msg.obj);
return true;
}
case MSG_STOP: {
stopInternal();
return true;
}
case MSG_RELEASE: {
releaseInternal();
return true;
}
case MSG_CUSTOM: {
sendMessageInternal(msg.arg1, msg.obj);
return true;
}
case MSG_SET_RENDERER_ENABLED: {
setRendererEnabledInternal(msg.arg1, msg.arg2 != 0);
return true;
}
default:
return false;
}
} catch (ExoPlaybackException e) {
Log.e(TAG, "Internal track renderer error.", e);
eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
stopInternal();
return true;
} catch (RuntimeException e) {
Log.e(TAG, "Internal runtime error.", e);
eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e)).sendToTarget();
stopInternal();
return true;
}
}
private void setState(int state) {
if (this.state != state) {
this.state = state;
eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
}
}
private void prepareInternal(TrackRenderer[] renderers) {
rebuffering = false;
this.renderers = renderers;
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].isTimeSource()) {
Assertions.checkState(timeSourceTrackRenderer == null);
timeSourceTrackRenderer = renderers[i];
}
}
setState(ExoPlayer.STATE_PREPARING);
handler.sendEmptyMessage(MSG_INCREMENTAL_PREPARE);
}
private void incrementalPrepareInternal() throws ExoPlaybackException {
long operationStartTimeMs = SystemClock.elapsedRealtime();
boolean prepared = true;
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
int state = renderers[i].prepare();
if (state == TrackRenderer.STATE_UNPREPARED) {
prepared = false;
}
}
}
if (!prepared) {
// We're still waiting for some sources to be prepared.
scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
return;
}
Log.d(TAG, "#####incrementalPrepareInternal(): state="+renderers[0].getState()+", renderers["+0+"]="+renderers[0].isEnded());
long durationUs = 0;
boolean isEnded = true;
boolean allRenderersReadyOrEnded = true;
for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i];
if (rendererEnabledFlags[i] && renderer.getState() == TrackRenderer.STATE_PREPARED) {
renderer.enable(positionUs, false);
enabledRenderers.add(renderer);
isEnded = isEnded && renderer.isEnded();
Log.d(TAG, "#####incrementalPrepareInternal(): isEnded="+isEnded+", renderers["+i+"]="+renderers[i].isEnded());
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
if (durationUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the duration is unknown, so the media
// duration is unknown regardless of the duration of this track.
} else {
long trackDurationUs = renderer.getDurationUs();
if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
durationUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
// Do nothing.
} else {
durationUs = Math.max(durationUs, trackDurationUs);
}
}
}
}
this.durationUs = durationUs;
System.out.println(">>>> ExoPlayerImplInternal.incrementalPrepareInternal() @ 311 isEnded = "+isEnded);
if (isEnded) {
// We don't expect this case, but handle it anyway.
Log.d(TAG, "#####incrementalPrepareInternal(): state="+state+", isEnded=>stop (we don't expect this case!!!) renderers[i]="+renderers[0].isEnded());
setState(ExoPlayer.STATE_ENDED);
} else {
setState(allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING);
if (playWhenReady && state == ExoPlayer.STATE_READY) {
startRenderers();
}
}
System.out.println("ExoPlayerImplInternal.incrementalPrepareInternal() send MSG_DO_SOME_WORK ***");
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private boolean rendererReadyOrEnded(TrackRenderer renderer) {
if (renderer.isEnded()) {
return true;
}
//Log.d(TAG, "#####rendererReadyOrEnded(): renderer.isReady()="+renderer.isReady()+", state_ready="+(state == ExoPlayer.STATE_READY));
if (!renderer.isReady()) {
return false;
}
if (state == ExoPlayer.STATE_READY) {
return true;
}
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
Log.d(TAG, "#####rendererReadyOrEnded(): rendererBufferedPositionUs="+rendererBufferedPositionUs+", rendererDurationUs="+rendererDurationUs+", positionUs="+positionUs+", minBufferDurationUs="+minBufferDurationUs);
/*Log.d(TAG, "#####rendererReadyOrEnded(): 1="+(minBufferDurationUs <= 0)+", 2="+(rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US)+", 3="+(rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US)+", 4="+(rendererBufferedPositionUs >= positionUs + minBufferDurationUs)+", 5="+((rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs)));*/
return minBufferDurationUs <= 0
|| rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US
|| rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| rendererBufferedPositionUs >= positionUs + minBufferDurationUs
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs);
}
private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
try {
System.out.println(">>>> ExoPlayerImplInternal.setPlayWhenReadyInternal() playWhenReady = "+playWhenReady + " state = "+state);
rebuffering = false;
this.playWhenReady = playWhenReady;
if (!playWhenReady) {
stopRenderers();
updatePositionUs();
} else {
if (state == ExoPlayer.STATE_READY) {
startRenderers();
System.out.println("ExoPlayerImplInternal.setPlayWhenReadyInternal() send MSG_DO_SOME_WORK ***");
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else if (state == ExoPlayer.STATE_BUFFERING) {
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
} finally {
eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget();
}
}
private void startRenderers() throws ExoPlaybackException {
rebuffering = false;
mediaClock.start();
Log.d("ExoPlayerImplInternal", "startRenderers(): #####" + Arrays.toString(Thread.currentThread().getStackTrace()));
System.out.println(">>ExoPlayerImplInternal.startRenderers() enabledRenderers="+enabledRenderers.size()+"/ "+enabledRenderers);
for (int i = 0; i < enabledRenderers.size(); i++) {
System.out.println(">>ExoPlayerImplInternal.startRenderers() STARTING RENDERER "+(enabledRenderers.get(i) != null? enabledRenderers.get(i).getClass().getName():"null"));
enabledRenderers.get(i).start();
}
}
private void stopRenderers() throws ExoPlaybackException {
mediaClock.stop();
for (int i = 0; i < enabledRenderers.size(); i++) {
ensureStopped(enabledRenderers.get(i));
}
}
private void updatePositionUs() {
if (timeSourceTrackRenderer != null && enabledRenderers.contains(timeSourceTrackRenderer)
&& !timeSourceTrackRenderer.isEnded()) {
positionUs = timeSourceTrackRenderer.getCurrentPositionUs();
mediaClock.setPositionUs(positionUs);
} else {
positionUs = mediaClock.getPositionUs();
}
elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
}
private void doSomeWork() throws ExoPlaybackException {
TraceUtil.beginSection("doSomeWork");
long operationStartTimeMs = SystemClock.elapsedRealtime();
long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs
: Long.MAX_VALUE;
boolean isEnded = true;
boolean allRenderersReadyOrEnded = true;
updatePositionUs();
for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i);
// TODO: Each renderer should return the maximum delay before which it wishes to be
// invoked again. The minimum of these values should then be used as the delay before the next
// invocation of this method.
renderer.doSomeWork(positionUs, elapsedRealtimeUs);
isEnded = isEnded && renderer.isEnded();
allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
//if( state<ExoPlayer.STATE_READY)
//Log.d(TAG, "#####doSomeWork(): ... "+i+"="+rendererReadyOrEnded(renderer));
if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
// We've already encountered a track for which the buffered position is unknown. Hence the
// media buffer position unknown regardless of the buffered position of this track.
} else {
long rendererDurationUs = renderer.getDurationUs();
long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
} else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
|| (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
&& rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
&& rendererBufferedPositionUs >= rendererDurationUs)) {
// This track is fully buffered.
} else {
bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
}
}
}
this.bufferedPositionUs = bufferedPositionUs;
//Log.d(TAG, "#####doSomeWork(): state="+state+", allRenderersReadyOrEnded="+allRenderersReadyOrEnded+", buff="+this.bufferedPositionUs);
if (isEnded) {
Log.d(TAG, "#####doSomeWork(): state="+state+", isEnded=>stop");
setState(ExoPlayer.STATE_ENDED);
stopRenderers();
} else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
setState(ExoPlayer.STATE_READY);
if (playWhenReady) {
Log.d(TAG, "#####doSomeWork(): state="+state+", allRenderersReadyOrEnded="+allRenderersReadyOrEnded+", buff="+this.bufferedPositionUs);
startRenderers();
}
} else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
rebuffering = playWhenReady;
setState(ExoPlayer.STATE_BUFFERING);
stopRenderers();
}
//System.out.println("ExoPlayerImplInternal.doSomeWork() send MSG_DO_SOME_WORK ***");
handler.removeMessages(MSG_DO_SOME_WORK);
if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
} else if (!enabledRenderers.isEmpty()) {
scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
}
TraceUtil.endSection();
}
private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs,
long intervalMs) {
long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
if (nextOperationDelayMs <= 0) {
handler.sendEmptyMessage(operationType);
} else {
handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
}
}
private void seekToInternal(long positionMs) throws ExoPlaybackException {
rebuffering = false;
positionUs = positionMs * 1000L;
mediaClock.stop();
mediaClock.setPositionUs(positionUs);
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
return;
}
for (int i = 0; i < enabledRenderers.size(); i++) {
TrackRenderer renderer = enabledRenderers.get(i);
ensureStopped(renderer);
renderer.seekTo(positionUs);
}
setState(ExoPlayer.STATE_BUFFERING);
System.out.println("ExoPlayerImplInternal.seekToInternal() send MSG_DO_SOME_WORK ***");
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
private void stopInternal() {
rebuffering = false;
resetInternal();
}
private void releaseInternal() {
resetInternal();
synchronized (this) {
released = true;
notifyAll();
}
}
private void resetInternal() {
System.out.println("ExoPlayerImplInternal.resetInternal() send MSG_DO_SOME_WORK ***");
Thread.dumpStack();
handler.removeMessages(MSG_DO_SOME_WORK);
handler.removeMessages(MSG_INCREMENTAL_PREPARE);
mediaClock.stop();
if (renderers == null) {
return;
}
for (int i = 0; i < renderers.length; i++) {
TrackRenderer renderer = renderers[i];
stopAndDisable(renderer);
release(renderer);
}
renderers = null;
timeSourceTrackRenderer = null;
enabledRenderers.clear();
setState(ExoPlayer.STATE_IDLE);
}
private void stopAndDisable(TrackRenderer renderer) {
try {
ensureStopped(renderer);
if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
renderer.disable();
}
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Stop failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Stop failed.", e);
}
}
private void release(TrackRenderer renderer) {
try {
renderer.release();
} catch (ExoPlaybackException e) {
// There's nothing we can do.
Log.e(TAG, "Release failed.", e);
} catch (RuntimeException e) {
// Ditto.
Log.e(TAG, "Release failed.", e);
}
}
private <T> void sendMessageInternal(int what, Object obj)
throws ExoPlaybackException {
try {
@SuppressWarnings("unchecked")
Pair<ExoPlayerComponent, Object> targetAndMessage = (Pair<ExoPlayerComponent, Object>) obj;
targetAndMessage.first.handleMessage(what, targetAndMessage.second);
} finally {
synchronized (this) {
customMessagesProcessed++;
notifyAll();
}
}
if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) {
System.out.println("ExoPlayerImplInternal.sendMessageInternal() send MSG_DO_SOME_WORK ***");
// The message may have caused something to change that now requires us to do work.
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
}
}
private void setRendererEnabledInternal(int index, boolean enabled)
throws ExoPlaybackException {
System.out.println(">>>>> ExoPlayerImplInternal.setRendererEnabledInternal() " +index +","+enabled);
if (rendererEnabledFlags[index] == enabled) {
return;
}
rendererEnabledFlags[index] = enabled;
if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
return;
}
TrackRenderer renderer = renderers[index];
int rendererState = renderer.getState();
if (rendererState != TrackRenderer.STATE_PREPARED &&
rendererState != TrackRenderer.STATE_ENABLED &&
rendererState != TrackRenderer.STATE_STARTED) {
return;
}
System.out.println(">>> ExoPlayerImplInternal.setRendererEnabledInternal() @591 enabled = "+enabled);
System.out.println(">>> ExoPlayerImplInternal.setRendererEnabledInternal() playwhe ready = "+playWhenReady);
if (enabled) {
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
System.out.println("ExoPlayerImplInternal.setRendererEnabledInternal()");
renderer.enable(positionUs, playing);
enabledRenderers.add(renderer);
if (playing) {
renderer.start();
}
System.out.println("ExoPlayerImplInternal.setRendererEnabledInternal() send MSG_DO_SOME_WORK ***");
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
} else {
if (renderer == timeSourceTrackRenderer) {
// We've been using timeSourceTrackRenderer to advance the current position, but it's
// being disabled. Sync mediaClock so that it can take over timing responsibilities.
mediaClock.setPositionUs(renderer.getCurrentPositionUs());
}
ensureStopped(renderer);
enabledRenderers.remove(renderer);
renderer.disable();
}
}
private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
if (renderer.getState() == TrackRenderer.STATE_STARTED) {
renderer.stop();
}
}
}