/**
   * Checks the video data.
   *
   * @return the number of bad frames
   */
  private int checkVideoData(VideoChunks inputData, MediaCodec decoder, OutputSurface surface) {
    final int TIMEOUT_USEC = 1000;
    ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
    ByteBuffer[] decoderOutputBuffers = decoder.getOutputBuffers();
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    int inputChunk = 0;
    int checkIndex = 0;
    int badFrames = 0;

    boolean outputDone = false;
    boolean inputDone = false;
    while (!outputDone) {
      if (VERBOSE) Log.d(TAG, "check loop");

      // Feed more data to the decoder.
      if (!inputDone) {
        int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
        if (inputBufIndex >= 0) {
          if (inputChunk == inputData.getNumChunks()) {
            // End of stream -- send empty frame with EOS flag set.
            decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            inputDone = true;
            if (VERBOSE) Log.d(TAG, "sent input EOS");
          } else {
            // Copy a chunk of input to the decoder.  The first chunk should have
            // the BUFFER_FLAG_CODEC_CONFIG flag set.
            ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
            inputBuf.clear();
            inputData.getChunkData(inputChunk, inputBuf);
            int flags = inputData.getChunkFlags(inputChunk);
            long time = inputData.getChunkTime(inputChunk);
            decoder.queueInputBuffer(inputBufIndex, 0, inputBuf.position(), time, flags);
            if (VERBOSE) {
              Log.d(
                  TAG,
                  "submitted frame "
                      + inputChunk
                      + " to dec, size="
                      + inputBuf.position()
                      + " flags="
                      + flags);
            }
            inputChunk++;
          }
        } else {
          if (VERBOSE) Log.d(TAG, "input buffer not available");
        }
      }

      if (!outputDone) {
        int decoderStatus = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
        if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
          // no output available yet
          if (VERBOSE) Log.d(TAG, "no output from decoder available");
        } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
          decoderOutputBuffers = decoder.getOutputBuffers();
          if (VERBOSE) Log.d(TAG, "decoder output buffers changed");
        } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
          MediaFormat newFormat = decoder.getOutputFormat();
          if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
        } else if (decoderStatus < 0) {
          fail("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
        } else { // decoderStatus >= 0
          ByteBuffer decodedData = decoderOutputBuffers[decoderStatus];

          if (VERBOSE)
            Log.d(
                TAG, "surface decoder given buffer " + decoderStatus + " (size=" + info.size + ")");
          if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            if (VERBOSE) Log.d(TAG, "output EOS");
            outputDone = true;
          }

          boolean doRender = (info.size != 0);

          // As soon as we call releaseOutputBuffer, the buffer will be forwarded
          // to SurfaceTexture to convert to a texture.  The API doesn't guarantee
          // that the texture will be available before the call returns, so we
          // need to wait for the onFrameAvailable callback to fire.
          decoder.releaseOutputBuffer(decoderStatus, doRender);
          if (doRender) {
            if (VERBOSE) Log.d(TAG, "awaiting frame " + checkIndex);
            assertEquals(
                "Wrong time stamp", computePresentationTime(checkIndex), info.presentationTimeUs);
            surface.awaitNewImage();
            surface.drawImage();
            if (!checkSurfaceFrame(checkIndex++)) {
              badFrames++;
            }
          }
        }
      }
    }

    return badFrames;
  }
  /** Edits a stream of video data. */
  private void editVideoData(
      VideoChunks inputData,
      MediaCodec decoder,
      OutputSurface outputSurface,
      InputSurface inputSurface,
      MediaCodec encoder,
      VideoChunks outputData) {
    final int TIMEOUT_USEC = 10000;
    ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
    ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    int inputChunk = 0;
    int outputCount = 0;

    boolean outputDone = false;
    boolean inputDone = false;
    boolean decoderDone = false;
    while (!outputDone) {
      if (VERBOSE) Log.d(TAG, "edit loop");

      // Feed more data to the decoder.
      if (!inputDone) {
        int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
        if (inputBufIndex >= 0) {
          if (inputChunk == inputData.getNumChunks()) {
            // End of stream -- send empty frame with EOS flag set.
            decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            inputDone = true;
            if (VERBOSE) Log.d(TAG, "sent input EOS (with zero-length frame)");
          } else {
            // Copy a chunk of input to the decoder.  The first chunk should have
            // the BUFFER_FLAG_CODEC_CONFIG flag set.
            ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
            inputBuf.clear();
            inputData.getChunkData(inputChunk, inputBuf);
            int flags = inputData.getChunkFlags(inputChunk);
            long time = inputData.getChunkTime(inputChunk);
            decoder.queueInputBuffer(inputBufIndex, 0, inputBuf.position(), time, flags);
            if (VERBOSE) {
              Log.d(
                  TAG,
                  "submitted frame "
                      + inputChunk
                      + " to dec, size="
                      + inputBuf.position()
                      + " flags="
                      + flags);
            }
            inputChunk++;
          }
        } else {
          if (VERBOSE) Log.d(TAG, "input buffer not available");
        }
      }

      // Assume output is available.  Loop until both assumptions are false.
      boolean decoderOutputAvailable = !decoderDone;
      boolean encoderOutputAvailable = true;
      while (decoderOutputAvailable || encoderOutputAvailable) {
        // Start by draining any pending output from the encoder.  It's important to
        // do this before we try to stuff any more data in.
        int encoderStatus = encoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
        if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
          // no output available yet
          if (VERBOSE) Log.d(TAG, "no output from encoder available");
          encoderOutputAvailable = false;
        } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
          encoderOutputBuffers = encoder.getOutputBuffers();
          if (VERBOSE) Log.d(TAG, "encoder output buffers changed");
        } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
          MediaFormat newFormat = encoder.getOutputFormat();
          if (VERBOSE) Log.d(TAG, "encoder output format changed: " + newFormat);
        } else if (encoderStatus < 0) {
          fail("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
        } else { // encoderStatus >= 0
          ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
          if (encodedData == null) {
            fail("encoderOutputBuffer " + encoderStatus + " was null");
          }

          // Write the data to the output "file".
          if (info.size != 0) {
            encodedData.position(info.offset);
            encodedData.limit(info.offset + info.size);

            outputData.addChunk(encodedData, info.flags, info.presentationTimeUs);
            outputCount++;

            if (VERBOSE) Log.d(TAG, "encoder output " + info.size + " bytes");
          }
          outputDone = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
          encoder.releaseOutputBuffer(encoderStatus, false);
        }
        if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
          // Continue attempts to drain output.
          continue;
        }

        // Encoder is drained, check to see if we've got a new frame of output from
        // the decoder.  (The output is going to a Surface, rather than a ByteBuffer,
        // but we still get information through BufferInfo.)
        if (!decoderDone) {
          int decoderStatus = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
          if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
            // no output available yet
            if (VERBOSE) Log.d(TAG, "no output from decoder available");
            decoderOutputAvailable = false;
          } else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            // decoderOutputBuffers = decoder.getOutputBuffers();
            if (VERBOSE) Log.d(TAG, "decoder output buffers changed (we don't care)");
          } else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // expected before first buffer of data
            MediaFormat newFormat = decoder.getOutputFormat();
            if (VERBOSE) Log.d(TAG, "decoder output format changed: " + newFormat);
          } else if (decoderStatus < 0) {
            fail("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
          } else { // decoderStatus >= 0
            if (VERBOSE)
              Log.d(
                  TAG,
                  "surface decoder given buffer " + decoderStatus + " (size=" + info.size + ")");
            // The ByteBuffers are null references, but we still get a nonzero
            // size for the decoded data.
            boolean doRender = (info.size != 0);

            // As soon as we call releaseOutputBuffer, the buffer will be forwarded
            // to SurfaceTexture to convert to a texture.  The API doesn't
            // guarantee that the texture will be available before the call
            // returns, so we need to wait for the onFrameAvailable callback to
            // fire.  If we don't wait, we risk rendering from the previous frame.
            decoder.releaseOutputBuffer(decoderStatus, doRender);
            if (doRender) {
              // This waits for the image and renders it after it arrives.
              if (VERBOSE) Log.d(TAG, "awaiting frame");
              outputSurface.awaitNewImage();
              outputSurface.drawImage();

              // Send it to the encoder.
              inputSurface.setPresentationTime(info.presentationTimeUs * 1000);
              if (VERBOSE) Log.d(TAG, "swapBuffers");
              inputSurface.swapBuffers();
            }
            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
              // forward decoder EOS to encoder
              if (VERBOSE) Log.d(TAG, "signaling input EOS");
              if (WORK_AROUND_BUGS) {
                // Bail early, possibly dropping a frame.
                return;
              } else {
                encoder.signalEndOfInputStream();
              }
            }
          }
        }
      }
    }

    if (inputChunk != outputCount) {
      throw new RuntimeException("frame lost: " + inputChunk + " in, " + outputCount + " out");
    }
  }