/*
 * 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.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.io.IOException;
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 TrackRenderer {

  /**
   * 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 SampleSource source;
  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 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 trackIndex;
  private int sourceState;
  private boolean inputStreamEnded;
  private boolean outputStreamEnded;
  private boolean waitingForKeys;
  private boolean waitingForFirstSyncFrame;
  private long currentPositionUs;

  /**
   * @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) {
    Assertions.checkState(Util.SDK_INT >= 16);
    this.source = source;
    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<Long>();
    outputBufferInfo = new MediaCodec.BufferInfo();
    codecReconfigurationState = RECONFIGURATION_STATE_NONE;
    codecReinitializationState = REINITIALIZATION_STATE_NONE;
  }

  @Override
  protected int doPrepare(long positionUs) throws ExoPlaybackException {
    try {
      boolean sourcePrepared = source.prepare(positionUs);
      if (!sourcePrepared) {
        return TrackRenderer.STATE_UNPREPARED;
      }
    } catch (IOException e) {
      throw new ExoPlaybackException(e);
    }

    for (int i = 0; i < source.getTrackCount(); i++) {
      // TODO: Right now this is getting the mime types of the container format
      // (e.g. audio/mp4 and video/mp4 for fragmented mp4). It needs to be getting the mime types
      // of the actual samples (e.g. audio/mp4a-latm and video/avc).
      if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
        trackIndex = i;
        return TrackRenderer.STATE_PREPARED;
      }
    }

    return TrackRenderer.STATE_IGNORE;
  }

  /**
   * Determines whether a mime type is handled by the renderer.
   *
   * @param mimeType The mime type to test.
   * @return True if the renderer can handle the mime type. False otherwise.
   */
  protected boolean handlesMimeType(String mimeType) {
    return true;
    // TODO: Uncomment once the TODO above is fixed.
    // DecoderInfoUtil.getDecoder(mimeType) != null;
  }

  @Override
  protected void onEnabled(long positionUs, boolean joining) {
    source.enable(trackIndex, positionUs);
    sourceState = SOURCE_STATE_NOT_READY;
    inputStreamEnded = false;
    outputStreamEnded = false;
    waitingForKeys = false;
    currentPositionUs = positionUs;
  }

  /**
   * 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.
   */
  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 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,
      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 decoderName = decoderInfo.name;
    codecIsAdaptive = decoderInfo.adaptive;
    try {
      long codecInitializingTimestamp = SystemClock.elapsedRealtime();
      codec = MediaCodec.createByCodecName(decoderName);
      configureCodec(codec, decoderName, format.getFrameworkMediaFormatV16(), mediaCrypto);
      codec.start();
      long codecInitializedTimestamp = SystemClock.elapsedRealtime();
      notifyDecoderInitialized(decoderName, codecInitializedTimestamp,
          codecInitializedTimestamp - codecInitializingTimestamp);
      inputBuffers = codec.getInputBuffers();
      outputBuffers = codec.getOutputBuffers();
    } catch (Exception e) {
      notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, decoderName));
    }
    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() {
    format = null;
    drmInitData = null;
    try {
      releaseCodec();
    } finally {
      try {
        if (openedDrmSession) {
          drmSessionManager.close();
          openedDrmSession = false;
        }
      } finally {
        source.disable(trackIndex);
      }
    }
  }

  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;
      codecReconfigurationState = RECONFIGURATION_STATE_NONE;
      codecReinitializationState = REINITIALIZATION_STATE_NONE;
      codecCounters.codecReleaseCount++;
      try {
        codec.stop();
      } finally {
        try {
          codec.release();
        } finally {
          codec = null;
        }
      }
    }
  }

  @Override
  protected void onReleased() {
    source.release();
  }

  @Override
  protected long getCurrentPositionUs() {
    return currentPositionUs;
  }

  @Override
  protected long getDurationUs() {
    return source.getTrackInfo(trackIndex).durationUs;
  }

  @Override
  protected long getBufferedPositionUs() {
    long sourceBufferedPosition = source.getBufferedPositionUs();
    return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US
        ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
  }

  @Override
  protected void seekTo(long positionUs) throws ExoPlaybackException {
    currentPositionUs = positionUs;
    source.seekToUs(positionUs);
    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 {
    try {
      sourceState = source.continueBuffering(positionUs)
          ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
          : SOURCE_STATE_NOT_READY;
      checkForDiscontinuity();
      if (format == null) {
        readFormat();
      }
      if (codec == null && shouldInitCodec()) {
        maybeInitCodec();
      }
      if (codec != null) {
        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
        if (feedInputBuffer(true)) {
          while (feedInputBuffer(false)) {}
        }
      }
      codecCounters.ensureUpdated();
    } catch (IOException e) {
      throw new ExoPlaybackException(e);
    }
  }

  private void readFormat() throws IOException, ExoPlaybackException {
    int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
    if (result == SampleSource.FORMAT_READ) {
      onInputFormatChanged(formatHolder);
    }
  }

  private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
    if (codec == null) {
      return;
    }
    int result = source.readData(trackIndex, currentPositionUs, 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();
    // Workaround for framework bugs.
    // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366].
    if (Util.SDK_INT >= 18 && codecReinitializationState == REINITIALIZATION_STATE_NONE) {
      codec.flush();
      codecHasQueuedBuffers = false;
    } else {
      releaseCodec();
      maybeInitCodec();
    }
    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 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 IOException If an error occurs reading data from the upstream source.
   * @throws ExoPlaybackException If an error occurs feeding the input buffer.
   */
  private boolean feedInputBuffer(boolean firstFeed) throws IOException, 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.data.clear();
    }

    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.
      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 = source.readData(trackIndex, currentPositionUs, 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.data.clear();
        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.data.clear();
        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
      }
      inputStreamEnded = true;
      try {
        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.data.clear();
        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 inputFormat The format of media input to the codec.
   * @param outputFormat The new output format.
   */
  protected void onOutputFormatChanged(MediaFormat inputFormat,
      android.media.MediaFormat outputFormat) {
    // 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(format, 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) {
      return false;
    }

    if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
      if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
        // We're waiting to re-initialize the codec, and have now received all final output buffers.
        releaseCodec();
        maybeInitCodec();
      } else {
        outputStreamEnded = true;
      }
      return false;
    }

    int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs);
    if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
        outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) {
      if (decodeOnlyIndex != -1) {
        decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
      } else {
        currentPositionUs = outputBufferInfo.presentationTimeUs;
      }
      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;

  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;
  }

}
