Fix release of DRM sessions

There were some edge cases in which we'd forget to release DRM
sessions. For example if we read a format and acquired a
pendingDrmSession (in onInputFormatChanged), then immediately
read another format and overwrote pendingDrmSession, we'd
forget to release the one that's been overwritten.

This change hopefully makes release much clearer. We keep a list
of all drm sessions we're currently holding. Whenever we update
either drmSession or pendingDrmSession, we release any other
sessions that are in the list.

PiperOrigin-RevId: 228905465
This commit is contained in:
olly 2019-01-11 18:39:27 +00:00 committed by Oliver Woodman
parent 71d4f39400
commit 2d30d66746
7 changed files with 148 additions and 130 deletions

View File

@ -127,8 +127,8 @@ public class LibvpxVideoRenderer extends BaseRenderer {
private VpxDecoder decoder; private VpxDecoder decoder;
private VpxInputBuffer inputBuffer; private VpxInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer; private VpxOutputBuffer outputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
private @ReinitializationState int decoderReinitializationState; private @ReinitializationState int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
@ -364,24 +364,10 @@ public class LibvpxVideoRenderer extends BaseRenderer {
clearReportedVideoSize(); clearReportedVideoSize();
clearRenderedFirstFrame(); clearRenderedFirstFrame();
try { try {
setSourceDrmSession(null);
releaseDecoder(); releaseDecoder();
} finally { } finally {
try { eventDispatcher.disabled(decoderCounters);
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
} }
} }
@ -433,18 +419,35 @@ public class LibvpxVideoRenderer extends BaseRenderer {
/** Releases the decoder. */ /** Releases the decoder. */
@CallSuper @CallSuper
protected void releaseDecoder() { protected void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null; inputBuffer = null;
outputBuffer = null; outputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
buffersInCodecCount = 0; buffersInCodecCount = 0;
if (decoder != null) {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
}
setDecoderDrmSession(null);
}
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
decoderDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
} }
/** /**
@ -467,16 +470,20 @@ public class LibvpxVideoRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); DrmSession<ExoMediaCrypto> session =
if (pendingDrmSession == drmSession) { drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
drmSessionManager.releaseSession(pendingDrmSession); if (session == decoderDrmSession || session == sourceDrmSession) {
// We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }
if (pendingDrmSession != drmSession) { if (sourceDrmSession != decoderDrmSession) {
if (decoderReceivedBuffers) { if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization. // Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
@ -704,12 +711,13 @@ public class LibvpxVideoRenderer extends BaseRenderer {
return; return;
} }
drmSession = pendingDrmSession; setDecoderDrmSession(sourceDrmSession);
ExoMediaCrypto mediaCrypto = null; ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) { if (decoderDrmSession != null) {
mediaCrypto = drmSession.getMediaCrypto(); mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used. // input format causes the session to be replaced before it's used.
@ -922,12 +930,12 @@ public class LibvpxVideoRenderer extends BaseRenderer {
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }

View File

@ -147,6 +147,7 @@ public interface AudioRendererEventListener {
* Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}.
*/ */
public void disabled(final DecoderCounters counters) { public void disabled(final DecoderCounters counters) {
counters.ensureUpdated();
if (listener != null) { if (listener != null) {
handler.post( handler.post(
() -> { () -> {

View File

@ -548,7 +548,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
try { try {
super.onDisabled(); super.onDisabled();
} finally { } finally {
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters); eventDispatcher.disabled(decoderCounters);
} }
} }

View File

@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
? extends AudioDecoderException> decoder; ? extends AudioDecoderException> decoder;
private DecoderInputBuffer inputBuffer; private DecoderInputBuffer inputBuffer;
private SimpleOutputBuffer outputBuffer; private SimpleOutputBuffer outputBuffer;
private DrmSession<ExoMediaCrypto> drmSession; @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
@ReinitializationState private int decoderReinitializationState; @ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
@ -462,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = decoderDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }
@ -568,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
waitingForKeys = false; waitingForKeys = false;
try { try {
setSourceDrmSession(null);
releaseDecoder(); releaseDecoder();
audioSink.reset(); audioSink.reset();
} finally { } finally {
try { eventDispatcher.disabled(decoderCounters);
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
} }
} }
@ -615,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return; return;
} }
drmSession = pendingDrmSession; setDecoderDrmSession(sourceDrmSession);
ExoMediaCrypto mediaCrypto = null; ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) { if (decoderDrmSession != null) {
mediaCrypto = drmSession.getMediaCrypto(); mediaCrypto = decoderDrmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); DrmSessionException drmError = decoderDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used. // input format causes the session to be replaced before it's used.
@ -646,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
private void releaseDecoder() { private void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null; inputBuffer = null;
outputBuffer = null; outputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
if (decoder != null) {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
}
setDecoderDrmSession(null);
}
private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
DrmSession<ExoMediaCrypto> previous = decoderDrmSession;
decoderDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<ExoMediaCrypto> session) {
if (session != null && session != decoderDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
} }
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
@ -671,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), DrmSession<ExoMediaCrypto> session =
inputFormat.drmInitData); drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
if (pendingDrmSession == drmSession) { if (session == decoderDrmSession || session == sourceDrmSession) {
drmSessionManager.releaseSession(pendingDrmSession); // We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }

View File

@ -287,13 +287,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private final DecoderInputBuffer flagsOnlyBuffer; private final DecoderInputBuffer flagsOnlyBuffer;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final TimedValueQueue<Format> formatQueue; private final TimedValueQueue<Format> formatQueue;
private final List<Long> decodeOnlyPresentationTimestamps; private final ArrayList<Long> decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo; private final MediaCodec.BufferInfo outputBufferInfo;
@Nullable private Format inputFormat; @Nullable private Format inputFormat;
private Format outputFormat; private Format outputFormat;
private DrmSession<FrameworkMediaCrypto> drmSession; @Nullable private DrmSession<FrameworkMediaCrypto> codecDrmSession;
private DrmSession<FrameworkMediaCrypto> pendingDrmSession; @Nullable private DrmSession<FrameworkMediaCrypto> sourceDrmSession;
private long renderTimeLimitMs; private long renderTimeLimitMs;
private float rendererOperatingRate; private float rendererOperatingRate;
@Nullable private MediaCodec codec; @Nullable private MediaCodec codec;
@ -457,14 +457,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
return; return;
} }
drmSession = pendingDrmSession; setCodecDrmSession(sourceDrmSession);
String mimeType = inputFormat.sampleMimeType; String mimeType = inputFormat.sampleMimeType;
MediaCrypto wrappedMediaCrypto = null; MediaCrypto wrappedMediaCrypto = null;
boolean drmSessionRequiresSecureDecoder = false; boolean drmSessionRequiresSecureDecoder = false;
if (drmSession != null) { if (codecDrmSession != null) {
FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto(); FrameworkMediaCrypto mediaCrypto = codecDrmSession.getMediaCrypto();
if (mediaCrypto == null) { if (mediaCrypto == null) {
DrmSessionException drmError = drmSession.getError(); DrmSessionException drmError = codecDrmSession.getError();
if (drmError != null) { if (drmError != null) {
// Continue for now. We may be able to avoid failure if the session recovers, or if a new // Continue for now. We may be able to avoid failure if the session recovers, or if a new
// input format causes the session to be replaced before it's used. // input format causes the session to be replaced before it's used.
@ -477,9 +478,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType);
} }
if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) {
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) {
// Wait for keys. // Wait for keys.
return; return;
@ -552,7 +553,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
inputFormat = null; inputFormat = null;
if (drmSession != null || pendingDrmSession != null) { if (codecDrmSession != null || sourceDrmSession != null) {
// TODO: Do something better with this case. // TODO: Do something better with this case.
onReset(); onReset();
} else { } else {
@ -565,51 +566,32 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
try { try {
releaseCodec(); releaseCodec();
} finally { } finally {
try { setSourceDrmSession(null);
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
}
}
} }
} }
protected void releaseCodec() { protected void releaseCodec() {
availableCodecInfos = null; availableCodecInfos = null;
if (codec != null) { codecInfo = null;
codecInfo = null; codecFormat = null;
codecFormat = null; resetInputBuffer();
resetInputBuffer(); resetOutputBuffer();
resetOutputBuffer(); resetCodecBuffers();
resetCodecBuffers(); waitingForKeys = false;
waitingForKeys = false; codecHotswapDeadlineMs = C.TIME_UNSET;
codecHotswapDeadlineMs = C.TIME_UNSET; decodeOnlyPresentationTimestamps.clear();
decodeOnlyPresentationTimestamps.clear(); try {
decoderCounters.decoderReleaseCount++; if (codec != null) {
try { decoderCounters.decoderReleaseCount++;
codec.stop();
} finally {
try { try {
codec.release(); codec.stop();
} finally { } finally {
codec = null; codec.release();
if (drmSession != null && pendingDrmSession != drmSession) {
try {
drmSessionManager.releaseSession(drmSession);
} finally {
drmSession = null;
}
}
} }
} }
} finally {
codec = null;
setCodecDrmSession(null);
} }
} }
@ -928,6 +910,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputBuffer = null; outputBuffer = null;
} }
private void setSourceDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
DrmSession<FrameworkMediaCrypto> previous = sourceDrmSession;
sourceDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void setCodecDrmSession(@Nullable DrmSession<FrameworkMediaCrypto> session) {
DrmSession<FrameworkMediaCrypto> previous = codecDrmSession;
codecDrmSession = session;
releaseDrmSessionIfUnused(previous);
}
private void releaseDrmSessionIfUnused(@Nullable DrmSession<FrameworkMediaCrypto> session) {
if (session != null && session != codecDrmSession && session != sourceDrmSession) {
drmSessionManager.releaseSession(session);
}
}
/** /**
* @return Whether it may be possible to feed more input data. * @return Whether it may be possible to feed more input data.
* @throws ExoPlaybackException If an error occurs feeding the input buffer. * @throws ExoPlaybackException If an error occurs feeding the input buffer.
@ -1082,12 +1082,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) {
return false; return false;
} }
@DrmSession.State int drmSessionState = drmSession.getState(); @DrmSession.State int drmSessionState = codecDrmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) { if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex());
} }
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
} }
@ -1126,13 +1126,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
throw ExoPlaybackException.createForRenderer( throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
} }
pendingDrmSession = DrmSession<FrameworkMediaCrypto> session =
drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData);
if (pendingDrmSession == drmSession) { if (session == codecDrmSession || session == sourceDrmSession) {
drmSessionManager.releaseSession(pendingDrmSession); // We already had this session. The manager must be reference counting, so release it once
// to get the count attributed to this renderer back down to 1.
drmSessionManager.releaseSession(session);
} }
setSourceDrmSession(session);
} else { } else {
pendingDrmSession = null; setSourceDrmSession(null);
} }
} }
@ -1143,7 +1146,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
// We have an existing codec that we may need to reconfigure or re-initialize. If the existing // We have an existing codec that we may need to reconfigure or re-initialize. If the existing
// codec instance is being kept then its operating rate may need to be updated. // codec instance is being kept then its operating rate may need to be updated.
if (pendingDrmSession != drmSession) { if (sourceDrmSession != codecDrmSession) {
drainAndReinitializeCodec(); drainAndReinitializeCodec();
} else { } else {
switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) {

View File

@ -375,7 +375,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
try { try {
super.onDisabled(); super.onDisabled();
} finally { } finally {
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters); eventDispatcher.disabled(decoderCounters);
} }
} }

View File

@ -179,6 +179,7 @@ public interface VideoRendererEventListener {
/** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */
public void disabled(DecoderCounters counters) { public void disabled(DecoderCounters counters) {
counters.ensureUpdated();
if (listener != null) { if (listener != null) {
handler.post( handler.post(
() -> { () -> {