/* * Copyright 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ package org.webrtc; import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Bundle; import android.support.annotation.Nullable; import android.view.Surface; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; /** * Fake MediaCodec that implements the basic state machine. * * @note This class is only intended for single-threaded tests and is not thread-safe. */ public class FakeMediaCodecWrapper implements MediaCodecWrapper { private static final int NUM_INPUT_BUFFERS = 10; private static final int NUM_OUTPUT_BUFFERS = 10; private static final int MAX_ENCODED_DATA_SIZE_BYTES = 1_000; /** * MediaCodec state as defined by: * https://developer.android.com/reference/android/media/MediaCodec.html */ public enum State { STOPPED_CONFIGURED(Primary.STOPPED), STOPPED_UNINITIALIZED(Primary.STOPPED), STOPPED_ERROR(Primary.STOPPED), EXECUTING_FLUSHED(Primary.EXECUTING), EXECUTING_RUNNING(Primary.EXECUTING), EXECUTING_END_OF_STREAM(Primary.EXECUTING), RELEASED(Primary.RELEASED); public enum Primary { STOPPED, EXECUTING, RELEASED } private final Primary primary; State(Primary primary) { this.primary = primary; } public Primary getPrimary() { return primary; } } /** Represents an output buffer that will be returned by dequeueOutputBuffer. */ public static class QueuedOutputBufferInfo { private int index; private int offset; private int size; private long presentationTimeUs; private int flags; private QueuedOutputBufferInfo( int index, int offset, int size, long presentationTimeUs, int flags) { this.index = index; this.offset = offset; this.size = size; this.presentationTimeUs = presentationTimeUs; this.flags = flags; } public static QueuedOutputBufferInfo create( int index, int offset, int size, long presentationTimeUs, int flags) { return new QueuedOutputBufferInfo(index, offset, size, presentationTimeUs, flags); } public int getIndex() { return index; } public int getOffset() { return offset; } public int getSize() { return size; } public long getPresentationTimeUs() { return presentationTimeUs; } public int getFlags() { return flags; } } private State state = State.STOPPED_UNINITIALIZED; private @Nullable MediaFormat configuredFormat; private int configuredFlags; private final MediaFormat outputFormat; private final ByteBuffer[] inputBuffers = new ByteBuffer[NUM_INPUT_BUFFERS]; private final ByteBuffer[] outputBuffers = new ByteBuffer[NUM_OUTPUT_BUFFERS]; private final boolean[] inputBufferReserved = new boolean[NUM_INPUT_BUFFERS]; private final boolean[] outputBufferReserved = new boolean[NUM_OUTPUT_BUFFERS]; private final List queuedOutputBuffers = new ArrayList<>(); public FakeMediaCodecWrapper(MediaFormat outputFormat) { this.outputFormat = outputFormat; } /** Returns the current simulated state of MediaCodec. */ public State getState() { return state; } /** Gets the last configured media format passed to configure. */ public @Nullable MediaFormat getConfiguredFormat() { return configuredFormat; } /** Returns the last flags passed to configure. */ public int getConfiguredFlags() { return configuredFlags; } /** * Adds a texture buffer that will be returned by dequeueOutputBuffer. Returns index of the * buffer. */ public int addOutputTexture(long presentationTimestampUs, int flags) { int index = getFreeOutputBuffer(); queuedOutputBuffers.add(QueuedOutputBufferInfo.create( index, /* offset= */ 0, /* size= */ 0, presentationTimestampUs, flags)); return index; } /** * Adds a byte buffer buffer that will be returned by dequeueOutputBuffer. Returns index of the * buffer. */ public int addOutputData(byte[] data, long presentationTimestampUs, int flags) { int index = getFreeOutputBuffer(); ByteBuffer outputBuffer = outputBuffers[index]; outputBuffer.clear(); outputBuffer.put(data); outputBuffer.rewind(); queuedOutputBuffers.add(QueuedOutputBufferInfo.create( index, /* offset= */ 0, data.length, presentationTimestampUs, flags)); return index; } /** * Returns the first output buffer that is not reserved and reserves it. It will be stay reserved * until released with releaseOutputBuffer. */ private int getFreeOutputBuffer() { for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { if (!outputBufferReserved[i]) { outputBufferReserved[i] = true; return i; } } throw new RuntimeException("All output buffers reserved!"); } @Override public void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags) { if (state != State.STOPPED_UNINITIALIZED) { throw new IllegalStateException("Expected state STOPPED_UNINITIALIZED but was " + state); } state = State.STOPPED_CONFIGURED; configuredFormat = format; configuredFlags = flags; final int width = configuredFormat.getInteger(MediaFormat.KEY_WIDTH); final int height = configuredFormat.getInteger(MediaFormat.KEY_HEIGHT); final int yuvSize = width * height * 3 / 2; final int inputBufferSize; final int outputBufferSize; if ((flags & MediaCodec.CONFIGURE_FLAG_ENCODE) != 0) { final int colorFormat = configuredFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT); inputBufferSize = colorFormat == CodecCapabilities.COLOR_FormatSurface ? 0 : yuvSize; outputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; } else { inputBufferSize = MAX_ENCODED_DATA_SIZE_BYTES; outputBufferSize = surface != null ? 0 : yuvSize; } for (int i = 0; i < inputBuffers.length; i++) { inputBuffers[i] = ByteBuffer.allocateDirect(inputBufferSize); } for (int i = 0; i < outputBuffers.length; i++) { outputBuffers[i] = ByteBuffer.allocateDirect(outputBufferSize); } } @Override public void start() { if (state != State.STOPPED_CONFIGURED) { throw new IllegalStateException("Expected state STOPPED_CONFIGURED but was " + state); } state = State.EXECUTING_RUNNING; } @Override public void flush() { if (state.getPrimary() != State.Primary.EXECUTING) { throw new IllegalStateException("Expected state EXECUTING but was " + state); } state = State.EXECUTING_FLUSHED; } @Override public void stop() { if (state.getPrimary() != State.Primary.EXECUTING) { throw new IllegalStateException("Expected state EXECUTING but was " + state); } state = State.STOPPED_UNINITIALIZED; } @Override public void release() { state = State.RELEASED; } @Override public int dequeueInputBuffer(long timeoutUs) { if (state != State.EXECUTING_FLUSHED && state != State.EXECUTING_RUNNING) { throw new IllegalStateException( "Expected state EXECUTING_FLUSHED or EXECUTING_RUNNING but was " + state); } state = State.EXECUTING_RUNNING; for (int i = 0; i < NUM_INPUT_BUFFERS; i++) { if (!inputBufferReserved[i]) { inputBufferReserved[i] = true; return i; } } return MediaCodec.INFO_TRY_AGAIN_LATER; } @Override public void queueInputBuffer( int index, int offset, int size, long presentationTimeUs, int flags) { if (state.getPrimary() != State.Primary.EXECUTING) { throw new IllegalStateException("Expected state EXECUTING but was " + state); } if (flags != 0) { throw new UnsupportedOperationException( "Flags are not implemented in FakeMediaCodecWrapper."); } } @Override public int dequeueOutputBuffer(MediaCodec.BufferInfo info, long timeoutUs) { if (state.getPrimary() != State.Primary.EXECUTING) { throw new IllegalStateException("Expected state EXECUTING but was " + state); } if (queuedOutputBuffers.isEmpty()) { return MediaCodec.INFO_TRY_AGAIN_LATER; } QueuedOutputBufferInfo outputBufferInfo = queuedOutputBuffers.remove(/* index= */ 0); info.set(outputBufferInfo.getOffset(), outputBufferInfo.getSize(), outputBufferInfo.getPresentationTimeUs(), outputBufferInfo.getFlags()); return outputBufferInfo.getIndex(); } @Override public void releaseOutputBuffer(int index, boolean render) { if (state.getPrimary() != State.Primary.EXECUTING) { throw new IllegalStateException("Expected state EXECUTING but was " + state); } if (!outputBufferReserved[index]) { throw new RuntimeException("Released output buffer was not in use."); } outputBufferReserved[index] = false; } @Override public ByteBuffer[] getInputBuffers() { return inputBuffers; } @Override public ByteBuffer[] getOutputBuffers() { return outputBuffers; } @Override public MediaFormat getOutputFormat() { return outputFormat; } @Override public Surface createInputSurface() { return new Surface(new SurfaceTexture(/* texName= */ 0)); } @Override public void setParameters(Bundle params) {} }