media/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
Oliver Woodman 6bf817f107 Workaround EOS propagation for all devices with RK decoder.
As per the end of the related issue, it's likely that all devices
running the affected API levels + decoder are affected by the same
issue.

Issue #464
2015-10-12 12:12:43 +01:00

943 lines
35 KiB
Java

/*
* 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.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer.drm.DrmInitData;
import com.google.android.exoplayer.drm.DrmSessionManager;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.TraceUtil;
import com.google.android.exoplayer.util.Util;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.media.MediaCodec.CryptoException;
import android.media.MediaCrypto;
import android.os.Handler;
import android.os.SystemClock;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
*/
@TargetApi(16)
public abstract class MediaCodecTrackRenderer extends SampleSourceTrackRenderer {
/**
* Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events.
*/
public interface EventListener {
/**
* Invoked when a decoder fails to initialize.
*
* @param e The corresponding exception.
*/
void onDecoderInitializationError(DecoderInitializationException e);
/**
* Invoked when a decoder operation raises a {@link CryptoException}.
*
* @param e The corresponding exception.
*/
void onCryptoError(CryptoException e);
/**
* Invoked when a decoder is successfully created.
*
* @param decoderName The decoder that was configured and created.
* @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the initialization
* finished.
* @param initializationDurationMs Amount of time taken to initialize the decoder.
*/
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
}
/**
* Thrown when a failure occurs instantiating a decoder.
*/
public static class DecoderInitializationException extends Exception {
private static final int CUSTOM_ERROR_CODE_BASE = -50000;
private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
/**
* The name of the decoder that failed to initialize. Null if no suitable decoder was found.
*/
public final String decoderName;
/**
* An optional developer-readable diagnostic information string. May be null.
*/
public final String diagnosticInfo;
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, int errorCode) {
super("Decoder init failed: [" + errorCode + "], " + mediaFormat, cause);
this.decoderName = null;
this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
}
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause,
String decoderName) {
super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
this.decoderName = decoderName;
this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
}
@TargetApi(21)
private static String getDiagnosticInfoV21(Throwable cause) {
if (cause instanceof CodecException) {
return ((CodecException) cause).getDiagnosticInfo();
}
return null;
}
private static String buildCustomDiagnosticInfo(int errorCode) {
String sign = errorCode < 0 ? "neg_" : "";
return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode);
}
}
/**
* Value returned by {@link #getSourceState()} when the source is not ready.
*/
protected static final int SOURCE_STATE_NOT_READY = 0;
/**
* Value returned by {@link #getSourceState()} when the source is ready and we're able to read
* from it.
*/
protected static final int SOURCE_STATE_READY = 1;
/**
* Value returned by {@link #getSourceState()} when the source is ready but we might not be able
* to read from it. We transition to this state when an attempt to read a sample fails despite the
* source reporting that samples are available. This can occur when the next sample to be provided
* by the source is for another renderer.
*/
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
/**
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
* time during which {@link #isReady()} will report true regardless of whether the new codec has
* output frames that are ready to be rendered.
* <p>
* This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
* other renderers, provided the new codec is able to decode some frames within this time period.
*/
private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
/**
* There is no pending adaptive reconfiguration work.
*/
private static final int RECONFIGURATION_STATE_NONE = 0;
/**
* Codec configuration data needs to be written into the next buffer.
*/
private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
/**
* Codec configuration data has been written into the next buffer, but that buffer still needs to
* be returned to the codec.
*/
private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
/**
* The codec does not need to be re-initialized.
*/
private static final int REINITIALIZATION_STATE_NONE = 0;
/**
* The input format has changed in a way that requires the codec to be re-initialized, but we
* haven't yet signaled an end of stream to the existing codec. We need to do so in order to
* ensure that it outputs any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The input format has changed in a way that requires the codec to be re-initialized, and we've
* signaled an end of stream to the existing codec. We're waiting for the codec to output an end
* of stream signal to indicate that it has output any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
public final CodecCounters codecCounters;
private final DrmSessionManager drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
private final SampleHolder sampleHolder;
private final MediaFormatHolder formatHolder;
private final List<Long> decodeOnlyPresentationTimestamps;
private final MediaCodec.BufferInfo outputBufferInfo;
private final EventListener eventListener;
protected final Handler eventHandler;
private MediaFormat format;
private DrmInitData drmInitData;
private MediaCodec codec;
private boolean codecIsAdaptive;
private boolean codecNeedsEosPropagationWorkaround;
private boolean codecNeedsEosFlushWorkaround;
private boolean codecReceivedEos;
private ByteBuffer[] inputBuffers;
private ByteBuffer[] outputBuffers;
private long codecHotswapTimeMs;
private int inputIndex;
private int outputIndex;
private boolean openedDrmSession;
private boolean codecReconfigured;
private int codecReconfigurationState;
private int codecReinitializationState;
private boolean codecHasQueuedBuffers;
private int sourceState;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeys;
private boolean waitingForFirstSyncFrame;
/**
* @param source The upstream source from which the renderer obtains samples.
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
* media is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
super(source);
Assertions.checkState(Util.SDK_INT >= 16);
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
this.eventHandler = eventHandler;
this.eventListener = eventListener;
codecCounters = new CodecCounters();
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
formatHolder = new MediaFormatHolder();
decodeOnlyPresentationTimestamps = new ArrayList<>();
outputBufferInfo = new MediaCodec.BufferInfo();
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
}
@Override
protected void onEnabled(int track, long positionUs, boolean joining)
throws ExoPlaybackException {
super.onEnabled(track, positionUs, joining);
seekToInternal();
}
/**
* Returns a {@link DecoderInfo} for decoding media in the specified MIME type.
*
* @param mimeType The type of media to decode.
* @param requiresSecureDecoder Whether a secure decoder is needed for decoding {@code mimeType}.
* @return {@link DecoderInfo} for decoding media in the specified MIME type, or {@code null} if
* no suitable decoder is available.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
protected DecoderInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
}
/**
* Configures a newly created {@link MediaCodec}. Sub-classes should override this method if they
* wish to configure the codec with a non-null surface.
*
* @param codec The {@link MediaCodec} to configure.
* @param codecName The name of the codec.
* @param codecIsAdaptive Whether the codec is adaptive.
* @param format The format for which the codec is being configured.
* @param crypto For drm protected playbacks, a {@link MediaCrypto} to use for decryption.
*/
protected void configureCodec(MediaCodec codec, String codecName, boolean codecIsAdaptive,
android.media.MediaFormat format, MediaCrypto crypto) {
codec.configure(format, null, crypto, 0);
}
@SuppressWarnings("deprecation")
protected final void maybeInitCodec() throws ExoPlaybackException {
if (!shouldInitCodec()) {
return;
}
String mimeType = format.mimeType;
MediaCrypto mediaCrypto = null;
boolean requiresSecureDecoder = false;
if (drmInitData != null) {
if (drmSessionManager == null) {
throw new ExoPlaybackException("Media requires a DrmSessionManager");
}
if (!openedDrmSession) {
drmSessionManager.open(drmInitData);
openedDrmSession = true;
}
int drmSessionState = drmSessionManager.getState();
if (drmSessionState == DrmSessionManager.STATE_ERROR) {
throw new ExoPlaybackException(drmSessionManager.getError());
} else if (drmSessionState == DrmSessionManager.STATE_OPENED
|| drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSessionManager.getMediaCrypto();
requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType);
} else {
// The drm session isn't open yet.
return;
}
}
DecoderInfo decoderInfo = null;
try {
decoderInfo = getDecoderInfo(mimeType, requiresSecureDecoder);
} catch (DecoderQueryException e) {
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e,
DecoderInitializationException.DECODER_QUERY_ERROR));
}
if (decoderInfo == null) {
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, null,
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
}
String codecName = decoderInfo.name;
codecIsAdaptive = decoderInfo.adaptive;
codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createByCodecName(" + codecName + ")");
codec = MediaCodec.createByCodecName(codecName);
TraceUtil.endSection();
TraceUtil.beginSection("configureCodec");
configureCodec(codec, codecName, codecIsAdaptive, format.getFrameworkMediaFormatV16(),
mediaCrypto);
TraceUtil.endSection();
TraceUtil.beginSection("codec.start()");
codec.start();
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
notifyDecoderInitialized(codecName, codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
inputBuffers = codec.getInputBuffers();
outputBuffers = codec.getOutputBuffers();
} catch (Exception e) {
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, codecName));
}
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
SystemClock.elapsedRealtime() : -1;
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
codecCounters.codecInitCount++;
}
private void notifyAndThrowDecoderInitError(DecoderInitializationException e)
throws ExoPlaybackException {
notifyDecoderInitializationError(e);
throw new ExoPlaybackException(e);
}
protected boolean shouldInitCodec() {
return codec == null && format != null;
}
protected final boolean codecInitialized() {
return codec != null;
}
protected final boolean haveFormat() {
return format != null;
}
@Override
protected void onDisabled() throws ExoPlaybackException {
format = null;
drmInitData = null;
try {
releaseCodec();
} finally {
try {
if (openedDrmSession) {
drmSessionManager.close();
openedDrmSession = false;
}
} finally {
super.onDisabled();
}
}
}
protected void releaseCodec() {
if (codec != null) {
codecHotswapTimeMs = -1;
inputIndex = -1;
outputIndex = -1;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear();
inputBuffers = null;
outputBuffers = null;
codecReconfigured = false;
codecHasQueuedBuffers = false;
codecIsAdaptive = false;
codecNeedsEosPropagationWorkaround = false;
codecNeedsEosFlushWorkaround = false;
codecReceivedEos = false;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
codecReinitializationState = REINITIALIZATION_STATE_NONE;
codecCounters.codecReleaseCount++;
try {
codec.stop();
} finally {
try {
codec.release();
} finally {
codec = null;
}
}
}
}
@Override
protected void seekTo(long positionUs) throws ExoPlaybackException {
super.seekTo(positionUs);
seekToInternal();
}
private void seekToInternal() {
sourceState = SOURCE_STATE_NOT_READY;
inputStreamEnded = false;
outputStreamEnded = false;
}
@Override
protected void onStarted() {
// Do nothing. Overridden to remove throws clause.
}
@Override
protected void onStopped() {
// Do nothing. Overridden to remove throws clause.
}
@Override
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
sourceState = continueBufferingSource(positionUs)
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
: SOURCE_STATE_NOT_READY;
checkForDiscontinuity(positionUs);
if (format == null) {
readFormat(positionUs);
}
if (codec == null && shouldInitCodec()) {
maybeInitCodec();
}
if (codec != null) {
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
if (feedInputBuffer(positionUs, true)) {
while (feedInputBuffer(positionUs, false)) {}
}
TraceUtil.endSection();
}
codecCounters.ensureUpdated();
}
private void readFormat(long positionUs) throws ExoPlaybackException {
int result = readSource(positionUs, formatHolder, sampleHolder, false);
if (result == SampleSource.FORMAT_READ) {
onInputFormatChanged(formatHolder);
}
}
private void checkForDiscontinuity(long positionUs) throws ExoPlaybackException {
if (codec == null) {
return;
}
int result = readSource(positionUs, formatHolder, sampleHolder, true);
if (result == SampleSource.DISCONTINUITY_READ) {
flushCodec();
}
}
private void flushCodec() throws ExoPlaybackException {
codecHotswapTimeMs = -1;
inputIndex = -1;
outputIndex = -1;
waitingForFirstSyncFrame = true;
waitingForKeys = false;
decodeOnlyPresentationTimestamps.clear();
if (Util.SDK_INT < 18 || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {
// Workaround framework bugs. See [Internal: b/8347958, b/8578467, b/8543366, b/23361053].
releaseCodec();
maybeInitCodec();
} else if (codecReinitializationState != REINITIALIZATION_STATE_NONE) {
// We're already waiting to release and re-initialize the codec. Since we're now flushing,
// there's no need to wait any longer.
releaseCodec();
maybeInitCodec();
} else {
// We can flush and re-use the existing decoder.
codec.flush();
codecHasQueuedBuffers = false;
}
if (codecReconfigured && format != null) {
// Any reconfiguration data that we send shortly before the flush may be discarded. We
// avoid this issue by sending reconfiguration data following every flush.
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
}
}
/**
* @param positionUs The current media time in microseconds, measured at the start of the
* current iteration of the rendering loop.
* @param firstFeed True if this is the first call to this method from the current invocation of
* {@link #doSomeWork(long, long)}. False otherwise.
* @return True if it may be possible to feed more input data. False otherwise.
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
*/
private boolean feedInputBuffer(long positionUs, boolean firstFeed) throws ExoPlaybackException {
if (inputStreamEnded
|| codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// The input stream has ended, or we need to re-initialize the codec but are still waiting
// for the existing codec to output any final output buffers.
return false;
}
if (inputIndex < 0) {
inputIndex = codec.dequeueInputBuffer(0);
if (inputIndex < 0) {
return false;
}
sampleHolder.data = inputBuffers[inputIndex];
sampleHolder.clearData();
}
if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
// We need to re-initialize the codec. Send an end of stream signal to the existing codec so
// that it outputs any remaining buffers before we release it.
if (codecNeedsEosPropagationWorkaround) {
// Do nothing.
} else {
codecReceivedEos = true;
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputIndex = -1;
}
codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
return false;
}
int result;
if (waitingForKeys) {
// We've already read an encrypted sample into sampleHolder, and are waiting for keys.
result = SampleSource.SAMPLE_READ;
} else {
// For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
// at the start of the buffer that also contains the first frame in the new format.
if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
for (int i = 0; i < format.initializationData.size(); i++) {
byte[] data = format.initializationData.get(i);
sampleHolder.data.put(data);
}
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
}
result = readSource(positionUs, formatHolder, sampleHolder, false);
if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
}
}
if (result == SampleSource.NOTHING_READ) {
return false;
}
if (result == SampleSource.DISCONTINUITY_READ) {
flushCodec();
return true;
}
if (result == SampleSource.FORMAT_READ) {
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// We received two formats in a row. Clear the current buffer of any reconfiguration data
// associated with the first format.
sampleHolder.clearData();
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
}
onInputFormatChanged(formatHolder);
return true;
}
if (result == SampleSource.END_OF_STREAM) {
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// We received a new format immediately before the end of the stream. We need to clear
// the corresponding reconfiguration data from the current buffer, but re-write it into
// a subsequent buffer if there are any (e.g. if the user seeks backwards).
sampleHolder.clearData();
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
}
inputStreamEnded = true;
if (!codecHasQueuedBuffers) {
processEndOfStream();
return false;
}
try {
if (codecNeedsEosPropagationWorkaround) {
// Do nothing.
} else {
codecReceivedEos = true;
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputIndex = -1;
}
} catch (CryptoException e) {
notifyCryptoError(e);
throw new ExoPlaybackException(e);
}
return false;
}
if (waitingForFirstSyncFrame) {
// TODO: Find out if it's possible to supply samples prior to the first sync
// frame for HE-AAC.
if (!sampleHolder.isSyncFrame()) {
sampleHolder.clearData();
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
// The buffer we just cleared contained reconfiguration data. We need to re-write this
// data into a subsequent buffer (if there is one).
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
}
return true;
}
waitingForFirstSyncFrame = false;
}
boolean sampleEncrypted = sampleHolder.isEncrypted();
waitingForKeys = shouldWaitForKeys(sampleEncrypted);
if (waitingForKeys) {
return false;
}
try {
int bufferSize = sampleHolder.data.position();
int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
long presentationTimeUs = sampleHolder.timeUs;
if (sampleHolder.isDecodeOnly()) {
decodeOnlyPresentationTimestamps.add(presentationTimeUs);
}
if (sampleEncrypted) {
MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder,
adaptiveReconfigurationBytes);
codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
} else {
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
}
inputIndex = -1;
codecHasQueuedBuffers = true;
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
} catch (CryptoException e) {
notifyCryptoError(e);
throw new ExoPlaybackException(e);
}
return true;
}
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder,
int adaptiveReconfigurationBytes) {
MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16();
if (adaptiveReconfigurationBytes == 0) {
return cryptoInfo;
}
// There must be at least one sub-sample, although numBytesOfClearData is permitted to be
// null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
// bytes to the clear byte count of the first sub-sample.
if (cryptoInfo.numBytesOfClearData == null) {
cryptoInfo.numBytesOfClearData = new int[1];
}
cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
return cryptoInfo;
}
private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException {
if (!openedDrmSession) {
return false;
}
int drmManagerState = drmSessionManager.getState();
if (drmManagerState == DrmSessionManager.STATE_ERROR) {
throw new ExoPlaybackException(drmSessionManager.getError());
}
if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS &&
(sampleEncrypted || !playClearSamplesWithoutKeys)) {
return true;
}
return false;
}
/**
* Invoked when a new format is read from the upstream {@link SampleSource}.
*
* @param formatHolder Holds the new format.
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
*/
protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
MediaFormat oldFormat = format;
format = formatHolder.format;
drmInitData = formatHolder.drmInitData;
if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
codecReconfigured = true;
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
} else {
if (codecHasQueuedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
} else {
// There aren't any final output buffers, so perform re-initialization immediately.
releaseCodec();
maybeInitCodec();
}
}
}
/**
* Invoked when the output format of the {@link MediaCodec} changes.
* <p>
* The default implementation is a no-op.
*
* @param outputFormat The new output format.
*/
protected void onOutputFormatChanged(android.media.MediaFormat outputFormat) {
// Do nothing.
}
/**
* Invoked when the output stream ends, meaning that the last output buffer has been processed
* and the {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag has been propagated through the
* decoder.
* <p>
* The default implementation is a no-op.
*/
protected void onOutputStreamEnded() {
// Do nothing.
}
/**
* Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
* sending codec specific initialization data at the start of the next input buffer. If true is
* returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
* returned then the instance will be released, and a new instance will be created for the new
* format.
* <p>
* The default implementation returns false.
*
* @param codec The existing {@link MediaCodec} instance.
* @param codecIsAdaptive Whether the codec is adaptive.
* @param oldFormat The format for which the existing instance is configured.
* @param newFormat The new format.
* @return True if the existing instance can be reconfigured. False otherwise.
*/
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
MediaFormat oldFormat, MediaFormat newFormat) {
return false;
}
@Override
protected boolean isEnded() {
return outputStreamEnded;
}
@Override
protected boolean isReady() {
return format != null && !waitingForKeys
&& (sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod());
}
/**
* Gets the source state.
*
* @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
* {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
*/
protected final int getSourceState() {
return sourceState;
}
private boolean isWithinHotswapPeriod() {
return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS;
}
/**
* Returns the maximum time to block whilst waiting for a decoded output buffer.
*
* @return The maximum time to block, in microseconds.
*/
protected long getDequeueOutputBufferTimeoutUs() {
return 0;
}
/**
* @return True if it may be possible to drain more output data. False otherwise.
* @throws ExoPlaybackException If an error occurs draining the output buffer.
*/
@SuppressWarnings("deprecation")
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
throws ExoPlaybackException {
if (outputStreamEnded) {
return false;
}
if (outputIndex < 0) {
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, getDequeueOutputBufferTimeoutUs());
}
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
onOutputFormatChanged(codec.getOutputFormat());
codecCounters.outputFormatChangedCount++;
return true;
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
codecCounters.outputBuffersChangedCount++;
return true;
} else if (outputIndex < 0) {
if (codecNeedsEosPropagationWorkaround && (inputStreamEnded
|| codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM)) {
processEndOfStream();
return true;
}
return false;
}
if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
processEndOfStream();
return false;
}
int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs);
if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) {
if (decodeOnlyIndex != -1) {
decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
}
outputIndex = -1;
return true;
}
return false;
}
/**
* Processes the provided output buffer.
*
* @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no
* longer required. False otherwise.
* @throws ExoPlaybackException If an error occurs processing the output buffer.
*/
protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex,
boolean shouldSkip) throws ExoPlaybackException;
/**
* Processes an end of stream signal.
*
* @throws ExoPlaybackException If an error occurs processing the signal.
*/
private void processEndOfStream() throws ExoPlaybackException {
if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// We're waiting to re-initialize the codec, and have now processed all final buffers.
releaseCodec();
maybeInitCodec();
} else {
outputStreamEnded = true;
onOutputStreamEnded();
}
}
private void notifyDecoderInitializationError(final DecoderInitializationException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDecoderInitializationError(e);
}
});
}
}
private void notifyCryptoError(final CryptoException e) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onCryptoError(e);
}
});
}
}
private void notifyDecoderInitialized(final String decoderName,
final long initializedTimestamp, final long initializationDuration) {
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDecoderInitialized(decoderName, initializedTimestamp,
initializationDuration);
}
});
}
}
private int getDecodeOnlyIndex(long presentationTimeUs) {
final int size = decodeOnlyPresentationTimestamps.size();
for (int i = 0; i < size; i++) {
if (decodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) {
return i;
}
}
return -1;
}
/**
* Returns whether the decoder is known to handle the propagation of the
* {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} flag incorrectly on the host device.
* <p>
* If true is returned, the renderer will work around the issue by approximating end of stream
* behavior without relying on the flag being propagated through to an output buffer by the
* underlying decoder.
*
* @param name The name of the decoder.
* @return True if the decoder is known to handle {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM}
* propagation incorrectly on the host device. False otherwise.
*/
private static boolean codecNeedsEosPropagationWorkaround(String name) {
return Util.SDK_INT <= 17 && "OMX.rk.video_decoder.avc".equals(name);
}
/**
* Returns whether the decoder is known to behave incorrectly if flushed after receiving an input
* buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set.
* <p>
* If true is returned, the renderer will work around the issue by instantiating a new decoder
* when this case occurs.
*
* @param name The name of the decoder.
* @return True if the decoder is known to behave incorrectly if flushed after receiving an input
* buffer with {@link MediaCodec#BUFFER_FLAG_END_OF_STREAM} set. False otherwise.
*/
private static boolean codecNeedsEosFlushWorkaround(String name) {
return Util.SDK_INT <= 23 && "OMX.google.vorbis.decoder".equals(name);
}
}