/* * 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.NalUnitUtil; 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.media.MediaFormat; 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 mime type for which a decoder was being initialized. */ public final String mimeType; /** * Whether it was required that the decoder support a secure output path. */ public final boolean secureDecoderRequired; /** * 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(Format format, Throwable cause, boolean secureDecoderRequired, int errorCode) { super("Decoder init failed: [" + errorCode + "], " + format, cause); this.mimeType = format.sampleMimeType; this.secureDecoderRequired = secureDecoderRequired; this.decoderName = null; this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode); } public DecoderInitializationException(Format format, Throwable cause, boolean secureDecoderRequired, String decoderName) { super("Decoder init failed: " + decoderName + ", " + format, cause); this.mimeType = format.sampleMimeType; this.secureDecoderRequired = secureDecoderRequired; 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. *
* 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 MediaCodecSelector mediaCodecSelector;
private final DrmSessionManager drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
private final SampleHolder sampleHolder;
private final FormatHolder formatHolder;
private final List
* The default implementation is a no-op.
*
* @param outputFormat The new output format.
* @throws ExoPlaybackException If an error occurs on output format change.
*/
protected void onOutputFormatChanged(MediaFormat outputFormat) throws ExoPlaybackException {
// 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.
*
* The default implementation is a no-op.
*/
protected void onOutputStreamEnded() {
// Do nothing.
}
/**
* Invoked when an input buffer is queued into the codec.
*
* The default implementation is a no-op.
*
* @param presentationTimeUs The timestamp associated with the input buffer.
*/
protected void onQueuedInputBuffer(long presentationTimeUs) {
// Do nothing.
}
/**
* Invoked when an output buffer is successfully processed.
*
* The default implementation is a no-op.
*
* @param presentationTimeUs The timestamp associated with the output buffer.
*/
protected void onProcessedOutputBuffer(long presentationTimeUs) {
// 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.
*
* 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, Format oldFormat,
Format 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) {
processOutputFormat();
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)) {
onProcessedOutputBuffer(outputBufferInfo.presentationTimeUs);
if (decodeOnlyIndex != -1) {
decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
}
outputIndex = -1;
return true;
}
return false;
}
/**
* Processes a new output format.
*
* @throws ExoPlaybackException If an error occurs processing the output format.
*/
private void processOutputFormat() throws ExoPlaybackException {
android.media.MediaFormat format = codec.getOutputFormat();
if (codecNeedsMonoChannelCountWorkaround) {
format.setInteger(android.media.MediaFormat.KEY_CHANNEL_COUNT, 1);
}
onOutputFormatChanged(format);
codecCounters.outputFormatChangedCount++;
}
/**
* 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 fail when flushed.
*
* If true is returned, the renderer will work around the issue by releasing the decoder and
* instantiating a new one rather than flushing the current instance.
*
* @param name The name of the decoder.
* @return True if the decoder is known to fail when flushed.
*/
private static boolean codecNeedsFlushWorkaround(String name) {
return Util.SDK_INT < 18
|| (Util.SDK_INT == 18
&& ("OMX.SEC.avc.dec".equals(name) || "OMX.SEC.avc.dec.secure".equals(name)))
|| (Util.SDK_INT == 19 && Util.MODEL.startsWith("SM-G800")
&& ("OMX.Exynos.avc.dec".equals(name) || "OMX.Exynos.avc.dec.secure".equals(name)));
}
/**
* Returns whether the decoder is an H.264/AVC decoder known to fail if NAL units are queued
* before the codec specific data.
*
* If true is returned, the renderer will work around the issue by discarding data up to the SPS.
*
* @param name The name of the decoder.
* @param format The format used to configure the decoder.
* @return True if the decoder is known to fail if NAL units are queued before CSD.
*/
private static boolean codecNeedsDiscardToSpsWorkaround(String name, Format format) {
return Util.SDK_INT < 21 && format.initializationData.isEmpty()
&& "OMX.MTK.VIDEO.DECODER.AVC".equals(name);
}
/**
* 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.
*
* 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.
*
* 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);
}
/**
* Returns whether the decoder is known to set the number of audio channels in the output format
* to 2 for the given input format, whilst only actually outputting a single channel.
*
* If true is returned then we explicitly override the number of channels in the output format,
* setting it to 1.
*
* @param name The decoder name.
* @param format The input format.
* @return True if the device is known to set the number of audio channels in the output format
* to 2 for the given input format, whilst only actually outputting a single channel. False
* otherwise.
*/
private static boolean codecNeedsMonoChannelCountWorkaround(String name, Format format) {
return Util.SDK_INT <= 18 && format.channelCount == 1
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
}
}